//
//  FWCloudSwift.swift
//  SwiftDemo
//
//  Created by amovision on 2024/12/3.
//

import Foundation
//
//  HMCharacteristicExtension.swift
//  LNDU
//
//  Created by michael on 2024/5/11.
//

import HomeKit

struct HMDeviceInformation {
    let name: String
    let manufacturer: String
    let serialNumber: String
    let identify: Bool
    let firmwareVersion: String
    let model: String
    let hardwareVersion: String
    init(name: String, manufacturer: String?, serialNumber: String?, identify: Bool? ,firmwareVersion: String?, model: String?, hardwareVersion: String?) {
        self.name = name
        self.manufacturer = manufacturer ?? ""
        self.serialNumber = serialNumber ?? ""
        self.identify = identify ?? false
        self.firmwareVersion = firmwareVersion ?? ""
        self.model = model ?? ""
        self.hardwareVersion = hardwareVersion ?? ""
    }
    init(name: String) {
        self.init(name: name, manufacturer: nil, serialNumber: nil, identify: nil, firmwareVersion: nil, model: nil, hardwareVersion: nil)
    }
}

struct HMLockInformation {
    let targetLocked: Bool
    let currentLocked: Bool
}

extension HMCharacteristic {
    var stateNames: [String] {
        switch characteristicType {
        case HMCharacteristicTypeCurrentLockMechanismState: return ["Unsecured", "Secured", "Jammed", "Unknown"]
        case HMCharacteristicTypeTargetLockMechanismState: return ["Not Locked", "Locked"]
        default: return []
        }
    }
    
    /// Indicates if you can write to the characteristic.
    var isWriteable: Bool {
        return properties.contains(HMCharacteristicPropertyWritable)
    }
    
    /// Indicates if you can read from the characteristic.
    var isReadable: Bool {
        return properties.contains(HMCharacteristicPropertyReadable)
    }
    
    /// Indicates if the characteristic is both readable and writable.
    var isReadWrite: Bool {
        return isReadable && isWriteable
    }
    
    /// Indicates that the characteristic value is a floating point number.
    var isFloat: Bool {
        return metadata?.format == HMCharacteristicMetadataFormatFloat
    }
    
    /// Indicates that the characteristic value is a signed integer.
    var isInt: Bool {
        return metadata?.format == HMCharacteristicMetadataFormatInt
    }
    
    /// Indicates that the characteristic value is an unsigned integer.
    var isUInt: Bool {
        return metadata?.format == HMCharacteristicMetadataFormatUInt8
            || metadata?.format == HMCharacteristicMetadataFormatUInt16
            || metadata?.format == HMCharacteristicMetadataFormatUInt32
            || metadata?.format == HMCharacteristicMetadataFormatUInt64
        
    }
    
    /// Indicates that the characteristic value is a number.
    var isNumeric: Bool {
        return isInt || isFloat || isUInt
    }
    
    /// Indicates that the characteristic value is a Boolean.
    var isBool: Bool {
        return metadata?.format == HMCharacteristicMetadataFormatBool
    }
    
    /// The difference between the characteristic’s maximum and minimum values.
    var span: Decimal {
        guard let max = metadata?.maximumValue?.decimalValue,
            let min = metadata?.minimumValue?.decimalValue  else {
                return 1.0
        }
        return max - min
    }
    
    /// The characteristic value as a float.
    var floatValue: Float? {
        return (value as? NSNumber)?.floatValue
    }
    
    /// The characteristic value as a decimal value.
    var decimalValue: Decimal? {
        return (value as? NSNumber)?.decimalValue
    }
    
    /// Returns the value that you get by mapping the given fraction
    /// to the characteristic's span, accounting for step size.
    func valueFor(fraction: Float) -> Decimal {
        let interval = metadata?.stepValue?.decimalValue ?? 1.0
        var inVal = (Decimal(Double(fraction)) * span) / interval
        var outVal: Decimal = 0.0
        NSDecimalRound(&outVal, &inVal, 0, .plain)
        return outVal * interval
    }
    
    /// A name that best represents the characteristic in the UI.
    var displayName: String {
        return metadata?.manufacturerDescription ?? localizedDescription
    }
    
    /// A string that represents the characteristic’s value.
    var formattedValueString: String {
        guard
            let value = value,
            let metadata = metadata else { return "—" }
        
        // Use the metadata to drive the string formatting.
        switch metadata.format {
        case HMCharacteristicMetadataFormatString:
            return value as? String ?? "—"
            
        case HMCharacteristicMetadataFormatInt,
             HMCharacteristicMetadataFormatUInt8,
             HMCharacteristicMetadataFormatUInt16,
             HMCharacteristicMetadataFormatUInt32,
             HMCharacteristicMetadataFormatUInt64:
            guard let intValue = value as? Int else { return "—" }
            guard stateNames.isEmpty else { return stateNames[intValue] }
            return String(intValue)
            
        case HMCharacteristicMetadataFormatFloat:
            guard let floatValue = floatValue else { return "—" }
            return String(format: "%.1f", floatValue)
            
        case HMCharacteristicMetadataFormatBool:
            guard let boolValue = value as? Bool else { return "—" }
            return boolValue ? "YES" : "NO"
            
        default:
            return "—"
        }
    }
}

extension HMAccessory {
    var batteryService: HMService? {
        services.first(where: { $0.serviceType == HMServiceTypeBattery })
    }
    
    var lockService: HMService? {
        services.first(where: { $0.serviceType == HMServiceTypeLockMechanism })
    }
    
    var infoService: HMService? {
        services.first(where: { $0.serviceType == HMServiceTypeAccessoryInformation })
    }
    
    var home: HMHome? {
        HomeStore.shared.homeManager.homes.first(where: { $0.accessories.contains(self) })
    }
    var battery: APBattery {
        if let batteryService {
            var status = 2
            var value = 0
            if let power = batteryService.batteryCharacteristic?.int {
                value = power
                let isLowPower = batteryService.lowBatteryCharacteristic?.bool ?? false
                let isCharging = batteryService.chargingStateCharacteristic?.bool ?? false
                if isLowPower { status = 4 }
                if isCharging { status = 1 }
            }else {
                status = 6
            }
            return APBattery(value: value, status: status)
        }else {
            return APBattery(value: 0)
        }
    }
    var deviceInfo: HMDeviceInformation {
        HMDeviceInformation(name: name, manufacturer: infoService?.mfrCharacteristic?.string, serialNumber: infoService?.snCharacteristic?.string, identify: infoService?.identifierCharacteristic?.bool, firmwareVersion: infoService?.firmwareCharacteristic?.string, model: infoService?.modelCharacteristic?.string, hardwareVersion: infoService?.hardwareCharacteristic?.string)
    }
    var lockInfo: HMLockInformation {
        HMLockInformation(targetLocked: (lockService?.targetLockCharacteristic?.int ?? 0) == 1, currentLocked: (lockService?.currentLockCharacteristic?.int ?? 0) == 1)
    }
}

extension HMService {
    
    var batteryCharacteristic: HMCharacteristic? {
        characteristics.first(where: { $0.characteristicType == HMCharacteristicTypeBatteryLevel })
    }
    var chargingStateCharacteristic: HMCharacteristic? {
        characteristics.first(where: { $0.characteristicType == HMCharacteristicTypeChargingState })
    }
    
    var lowBatteryCharacteristic: HMCharacteristic? {
        characteristics.first(where: { $0.characteristicType == HMCharacteristicTypeStatusLowBattery })
    }
    
    var nameCharacteristic: HMCharacteristic? {
        characteristics.first(where: { $0.characteristicType == HMCharacteristicTypeName })
    }
    var identifierCharacteristic: HMCharacteristic? {
        characteristics.first(where: { $0.characteristicType == HMCharacteristicTypeIdentify })
    }
    var targetLockCharacteristic: HMCharacteristic? {
        characteristics.first(where: { $0.characteristicType == HMCharacteristicTypeTargetLockMechanismState })
    }
    var currentLockCharacteristic: HMCharacteristic? {
        characteristics.first(where: { $0.characteristicType == HMCharacteristicTypeCurrentLockMechanismState })
    }
    var modelCharacteristic: HMCharacteristic? {
        characteristics.first(where: { $0.characteristicType == HMCharacteristicTypeModel })
    }
    var mfrCharacteristic: HMCharacteristic? {
        characteristics.first(where: { $0.characteristicType == HMCharacteristicTypeManufacturer })
    }
    var snCharacteristic: HMCharacteristic? {
        characteristics.first(where: { $0.characteristicType == HMCharacteristicTypeSerialNumber })
    }
    var firmwareCharacteristic: HMCharacteristic? {
        characteristics.first(where: { $0.characteristicType == HMCharacteristicTypeFirmwareVersion })
    }
    var hardwareCharacteristic: HMCharacteristic? {
        characteristics.first(where: { $0.characteristicType == HMCharacteristicTypeHardwareVersion })
    }
}

extension HMCharacteristic {
    var int: Int? {
        value as? Int
    }
    
    var bool: Bool {
        (value as? Int) == 1
    }
    
    var string: String? {
        value as? String
    }
}


//
//  HomeKitTypes.swift
//  FuweiCloud
//
//  Created by michael on 2024/5/11.
//

import HomeKit

extension String {
    var serviceTypeName: String {
        switch self {
        case HMServiceTypeSwitch: return "HMServiceTypeSwitch"
        case HMServiceTypeThermostat: return "HMServiceTypeThermostat"
        case HMServiceTypeOutlet: return "HMServiceTypeOutlet"
        case HMServiceTypeLockManagement: return "HMServiceTypeLockManagement"
        case HMServiceTypeAirQualitySensor: return "HMServiceTypeAirQualitySensor"
        case HMServiceTypeCarbonDioxideSensor: return "HMServiceTypeCarbonDioxideSensor"
        case HMServiceTypeCarbonMonoxideSensor: return "HMServiceTypeCarbonMonoxideSensor"
        case HMServiceTypeContactSensor: return "HMServiceTypeContactSensor"
        case HMServiceTypeDoor: return "HMServiceTypeDoor"
        case HMServiceTypeHumiditySensor: return "HMServiceTypeHumiditySensor"
        case HMServiceTypeLeakSensor: return "HMServiceTypeLeakSensor"
        case HMServiceTypeLightSensor: return "HMServiceTypeLightSensor"
        case HMServiceTypeMotionSensor: return "HMServiceTypeMotionSensor"
        case HMServiceTypeOccupancySensor: return "HMServiceTypeOccupancySensor"
        case HMServiceTypeSecuritySystem: return "HMServiceTypeSecuritySystem"
        case HMServiceTypeStatefulProgrammableSwitch: return "HMServiceTypeStatefulProgrammableSwitch"
        case HMServiceTypeStatelessProgrammableSwitch: return "HMServiceTypeStatelessProgrammableSwitch"
        case HMServiceTypeSmokeSensor: return "HMServiceTypeSmokeSensor"
        case HMServiceTypeTemperatureSensor: return "HMServiceTypeTemperatureSensor"
        case HMServiceTypeWindow: return "HMServiceTypeWindow"
        case HMServiceTypeWindowCovering: return "HMServiceTypeWindowCovering"
        case HMServiceTypeCameraRTPStreamManagement: return "HMServiceTypeCameraRTPStreamManagement"
        case HMServiceTypeCameraControl: return "HMServiceTypeCameraControl"
        case HMServiceTypeMicrophone: return "HMServiceTypeMicrophone"
        case HMServiceTypeSpeaker: return "HMServiceTypeSpeaker"
        case HMServiceTypeAirPurifier: return "HMServiceTypeAirPurifier"
        case HMServiceTypeFilterMaintenance: return "HMServiceTypeFilterMaintenance"
        case HMServiceTypeSlats: return "HMServiceTypeSlats"
        case HMServiceTypeLabel: return "HMServiceTypeLabel"
        case HMServiceTypeIrrigationSystem: return "HMServiceTypeIrrigationSystem"
        case HMServiceTypeValve: return "HMServiceTypeValve"
        case HMServiceTypeFaucet: return "HMServiceTypeFaucet"
        case HMServiceTypeAccessoryInformation: return "HMServiceTypeAccessoryInformation"
        case HMServiceTypeFan: return "HMServiceTypeFan"
        case HMServiceTypeGarageDoorOpener: return "HMServiceTypeGarageDoorOpener"
        case HMServiceTypeLightbulb: return "HMServiceTypeLightbulb"
        case HMServiceTypeLockMechanism: return "HMServiceTypeLockMechanism"
        case HMServiceTypeBattery: return "HMServiceTypeBattery"
        case HMServiceTypeVentilationFan: return "HMServiceTypeVentilationFan"
        case HMServiceTypeHeaterCooler: return "HMServiceTypeHeaterCooler"
        case HMServiceTypeHumidifierDehumidifier: return "HMServiceTypeHumidifierDehumidifier"
        case HMServiceTypeDoorbell: return "HMServiceTypeDoorbell"
        default: return "HMServiceTypeUnknown"
        }
    }
    
    var characteristicTypeName: String {
        switch self {
        case HMCharacteristicPropertyReadable: return "HMCharacteristicPropertyReadable"
        case HMCharacteristicPropertyWritable: return "HMCharacteristicPropertyWritable"
        case HMCharacteristicPropertyHidden: return "HMCharacteristicPropertyHidden"
        case HMCharacteristicTypeTargetRelativeHumidity: return "HMCharacteristicTypeTargetRelativeHumidity"
        case HMCharacteristicTypeOutletInUse: return "HMCharacteristicTypeOutletInUse"
        case HMCharacteristicTypeLogs: return "HMCharacteristicTypeLogs"
        case HMCharacteristicTypeAudioFeedback: return "HMCharacteristicTypeAudioFeedback"
        case HMCharacteristicTypeAdminOnlyAccess: return "HMCharacteristicTypeAdminOnlyAccess"
        case HMCharacteristicTypeSecuritySystemAlarmType: return "HMCharacteristicTypeSecuritySystemAlarmType"
        case HMCharacteristicTypeMotionDetected: return "HMCharacteristicTypeMotionDetected"
        case HMCharacteristicTypeLockMechanismLastKnownAction: return "HMCharacteristicTypeLockMechanismLastKnownAction"
        case HMCharacteristicTypeLockManagementControlPoint: return "HMCharacteristicTypeLockManagementControlPoint"
        case HMCharacteristicTypeLockManagementAutoSecureTimeout: return "HMCharacteristicTypeLockManagementAutoSecureTimeout"
        case HMCharacteristicTypeAirParticulateDensity: return "HMCharacteristicTypeAirParticulateDensity"
        case HMCharacteristicTypeAirParticulateSize: return "HMCharacteristicTypeAirParticulateSize"
        case HMCharacteristicTypeAirQuality: return "HMCharacteristicTypeAirQuality"
        case HMCharacteristicTypeCarbonDioxideDetected: return "HMCharacteristicTypeCarbonDioxideDetected"
        case HMCharacteristicTypeCarbonDioxideLevel: return "HMCharacteristicTypeCarbonDioxideLevel"
        case HMCharacteristicTypeCarbonDioxidePeakLevel: return "HMCharacteristicTypeCarbonDioxidePeakLevel"
        case HMCharacteristicTypeCarbonMonoxideDetected: return "HMCharacteristicTypeCarbonMonoxideDetected"
        case HMCharacteristicTypeCarbonMonoxideLevel: return "HMCharacteristicTypeCarbonMonoxideLevel"
        case HMCharacteristicTypeCarbonMonoxidePeakLevel: return "HMCharacteristicTypeCarbonMonoxidePeakLevel"
        case HMCharacteristicTypeContactState: return "HMCharacteristicTypeContactState"
        case HMCharacteristicTypeCurrentHorizontalTilt: return "HMCharacteristicTypeCurrentHorizontalTilt"
        case HMCharacteristicTypeCurrentPosition: return "HMCharacteristicTypeCurrentPosition"
        case HMCharacteristicTypeCurrentSecuritySystemState: return "HMCharacteristicTypeCurrentSecuritySystemState"
        case HMCharacteristicTypeCurrentVerticalTilt: return "HMCharacteristicTypeCurrentVerticalTilt"
        case HMCharacteristicTypeHoldPosition: return "HMCharacteristicTypeHoldPosition"
        case HMCharacteristicTypeLeakDetected: return "HMCharacteristicTypeLeakDetected"
        case HMCharacteristicTypeOccupancyDetected: return "HMCharacteristicTypeOccupancyDetected"
        case HMCharacteristicTypeOutputState: return "HMCharacteristicTypeOutputState"
        case HMCharacteristicTypePositionState: return "HMCharacteristicTypePositionState"
        case HMCharacteristicTypeStatusActive: return "HMCharacteristicTypeStatusActive"
        case HMCharacteristicTypeStatusFault: return "HMCharacteristicTypeStatusFault"
        case HMCharacteristicTypeStatusJammed: return "HMCharacteristicTypeStatusJammed"
        case HMCharacteristicTypeStatusTampered: return "HMCharacteristicTypeStatusTampered"
        case HMCharacteristicTypeTargetHorizontalTilt: return "HMCharacteristicTypeTargetHorizontalTilt"
        case HMCharacteristicTypeTargetSecuritySystemState: return "HMCharacteristicTypeTargetSecuritySystemState"
        case HMCharacteristicTypeTargetPosition: return "HMCharacteristicTypeTargetPosition"
        case HMCharacteristicTypeTargetVerticalTilt: return "HMCharacteristicTypeTargetVerticalTilt"
        case HMCharacteristicTypeStreamingStatus: return "HMCharacteristicTypeStreamingStatus"
        case HMCharacteristicTypeSetupStreamEndpoint: return "HMCharacteristicTypeSetupStreamEndpoint"
        case HMCharacteristicTypeSupportedVideoStreamConfiguration: return "HMCharacteristicTypeSupportedVideoStreamConfiguration"
        case HMCharacteristicTypeSupportedRTPConfiguration: return "HMCharacteristicTypeSupportedRTPConfiguration"
        case HMCharacteristicTypeSelectedStreamConfiguration: return "HMCharacteristicTypeSelectedStreamConfiguration"
        case HMCharacteristicTypeOpticalZoom: return "HMCharacteristicTypeOpticalZoom"
        case HMCharacteristicTypeDigitalZoom: return "HMCharacteristicTypeDigitalZoom"
        case HMCharacteristicTypeImageRotation: return "HMCharacteristicTypeImageRotation"
        case HMCharacteristicTypeImageMirroring: return "HMCharacteristicTypeImageMirroring"
        case HMCharacteristicTypeLabelNamespace: return "HMCharacteristicTypeLabelNamespace"
        case HMCharacteristicTypeLabelIndex: return "HMCharacteristicTypeLabelIndex"
        case HMCharacteristicTypeCurrentAirPurifierState: return "HMCharacteristicTypeCurrentAirPurifierState"
        case HMCharacteristicTypeTargetAirPurifierState: return "HMCharacteristicTypeTargetAirPurifierState"
        case HMCharacteristicTypeCurrentSlatState: return "HMCharacteristicTypeCurrentSlatState"
        case HMCharacteristicTypeFilterChangeIndication: return "HMCharacteristicTypeFilterChangeIndication"
        case HMCharacteristicTypeFilterLifeLevel: return "HMCharacteristicTypeFilterLifeLevel"
        case HMCharacteristicTypeFilterResetChangeIndication: return "HMCharacteristicTypeFilterResetChangeIndication"
        case HMCharacteristicTypeSlatType: return "HMCharacteristicTypeSlatType"
        case HMCharacteristicTypeCurrentTilt: return "HMCharacteristicTypeCurrentTilt"
        case HMCharacteristicTypeTargetTilt: return "HMCharacteristicTypeTargetTilt"
        case HMCharacteristicTypeOzoneDensity: return "HMCharacteristicTypeOzoneDensity"
        case HMCharacteristicTypeNitrogenDioxideDensity: return "HMCharacteristicTypeNitrogenDioxideDensity"
        case HMCharacteristicTypeSulphurDioxideDensity: return "HMCharacteristicTypeSulphurDioxideDensity"
        case HMCharacteristicTypePM2_5Density: return "HMCharacteristicTypePM2_5Density"
        case HMCharacteristicTypePM10Density: return "HMCharacteristicTypePM10Density"
        case HMCharacteristicTypeVolatileOrganicCompoundDensity: return "HMCharacteristicTypeVolatileOrganicCompoundDensity"
        case HMCharacteristicTypeProgramMode: return "HMCharacteristicTypeProgramMode"
        case HMCharacteristicTypeInUse: return "HMCharacteristicTypeInUse"
        case HMCharacteristicTypeSetDuration: return "HMCharacteristicTypeSetDuration"
        case HMCharacteristicTypeRemainingDuration: return "HMCharacteristicTypeRemainingDuration"
        case HMCharacteristicTypeValveType: return "HMCharacteristicTypeValveType"
        case HMCharacteristicTypeBrightness: return "HMCharacteristicTypeBrightness"
        case HMCharacteristicTypeCoolingThreshold: return "HMCharacteristicTypeCoolingThreshold"
        case HMCharacteristicTypeCurrentDoorState: return "HMCharacteristicTypeCurrentDoorState"
        case HMCharacteristicTypeCurrentHeatingCooling: return "HMCharacteristicTypeCurrentHeatingCooling"
        case HMCharacteristicTypeCurrentRelativeHumidity: return "HMCharacteristicTypeCurrentRelativeHumidity"
        case HMCharacteristicTypeCurrentTemperature: return "HMCharacteristicTypeCurrentTemperature"
        case HMCharacteristicTypeHeatingThreshold: return "HMCharacteristicTypeHeatingThreshold"
        case HMCharacteristicTypeHue: return "HMCharacteristicTypeHue"
        case HMCharacteristicTypeIdentify: return "HMCharacteristicTypeIdentify"
        case HMCharacteristicTypeCurrentLockMechanismState: return "HMCharacteristicTypeCurrentLockMechanismState"
        case HMCharacteristicTypeTargetLockMechanismState: return "HMCharacteristicTypeTargetLockMechanismState"
        case HMCharacteristicTypeManufacturer: return "HMCharacteristicTypeManufacturer"
        case HMCharacteristicTypeModel: return "HMCharacteristicTypeModel"
        case HMCharacteristicTypeName: return "HMCharacteristicTypeName"
        case HMCharacteristicTypeObstructionDetected: return "HMCharacteristicTypeObstructionDetected"
        case HMCharacteristicTypePowerState: return "HMCharacteristicTypePowerState"
        case HMCharacteristicTypeRotationDirection: return "HMCharacteristicTypeRotationDirection"
        case HMCharacteristicTypeRotationSpeed: return "HMCharacteristicTypeRotationSpeed"
        case HMCharacteristicTypeSaturation: return "HMCharacteristicTypeSaturation"
        case HMCharacteristicTypeSerialNumber: return "HMCharacteristicTypeSerialNumber"
        case HMCharacteristicTypeTargetDoorState: return "HMCharacteristicTypeTargetDoorState"
        case HMCharacteristicTypeTargetHeatingCooling: return "HMCharacteristicTypeTargetHeatingCooling"
        case HMCharacteristicTypeTargetTemperature: return "HMCharacteristicTypeTargetTemperature"
        case HMCharacteristicTypeTemperatureUnits: return "HMCharacteristicTypeTemperatureUnits"
        case HMCharacteristicTypeVersion: return "HMCharacteristicTypeVersion"
        case HMCharacteristicTypeFirmwareVersion: return "HMCharacteristicTypeFirmwareVersion"
        case HMCharacteristicTypeHardwareVersion: return "HMCharacteristicTypeHardwareVersion"
        case HMCharacteristicTypeSoftwareVersion: return "HMCharacteristicTypeSoftwareVersion"
        case HMCharacteristicTypeBatteryLevel: return "HMCharacteristicTypeBatteryLevel"
        case HMCharacteristicTypeCurrentLightLevel: return "HMCharacteristicTypeCurrentLightLevel"
        case HMCharacteristicTypeInputEvent: return "HMCharacteristicTypeInputEvent"
        case HMCharacteristicTypeSmokeDetected: return "HMCharacteristicTypeSmokeDetected"
        case HMCharacteristicTypeStatusLowBattery: return "HMCharacteristicTypeStatusLowBattery"
        case HMCharacteristicTypeChargingState: return "HMCharacteristicTypeChargingState"
        case HMCharacteristicTypeLockPhysicalControls: return "HMCharacteristicTypeLockPhysicalControls"
        case HMCharacteristicTypeCurrentFanState: return "HMCharacteristicTypeCurrentFanState"
        case HMCharacteristicTypeActive: return "HMCharacteristicTypeActive"
        case HMCharacteristicTypeCurrentHeaterCoolerState: return "HMCharacteristicTypeCurrentHeaterCoolerState"
        case HMCharacteristicTypeTargetHeaterCoolerState: return "HMCharacteristicTypeTargetHeaterCoolerState"
        case HMCharacteristicTypeCurrentHumidifierDehumidifierState: return "HMCharacteristicTypeCurrentHumidifierDehumidifierState"
        case HMCharacteristicTypeTargetHumidifierDehumidifierState: return "HMCharacteristicTypeTargetHumidifierDehumidifierState"
        case HMCharacteristicTypeWaterLevel: return "HMCharacteristicTypeWaterLevel"
        case HMCharacteristicTypeSwingMode: return "HMCharacteristicTypeSwingMode"
        case HMCharacteristicTypeTargetFanState: return "HMCharacteristicTypeTargetFanState"
        case HMCharacteristicTypeDehumidifierThreshold: return "HMCharacteristicTypeDehumidifierThreshold"
        case HMCharacteristicTypeHumidifierThreshold: return "HMCharacteristicTypeHumidifierThreshold"
        case HMCharacteristicTypeColorTemperature: return "HMCharacteristicTypeColorTemperature"
        case HMCharacteristicTypeIsConfigured: return "HMCharacteristicTypeIsConfigured"
        case HMCharacteristicTypeSupportedAudioStreamConfiguration: return "HMCharacteristicTypeSupportedAudioStreamConfiguration"
        case HMCharacteristicTypeVolume: return "HMCharacteristicTypeVolume"
        case HMCharacteristicTypeMute: return "HMCharacteristicTypeMute"
        case HMCharacteristicTypeNightVision: return "HMCharacteristicTypeNightVision"
        default: return "HMCharacteristicTypeUnknown"
        }
    }
    
    var accessoryTypeName: String {
        switch self {
        case HMAccessoryCategoryTypeOther: return "HMAccessoryCategoryTypeOther"
        case HMAccessoryCategoryTypeSecuritySystem: return "HMAccessoryCategoryTypeSecuritySystem"
        case HMAccessoryCategoryTypeBridge: return "HMAccessoryCategoryTypeBridge"
        case HMAccessoryCategoryTypeDoor: return "HMAccessoryCategoryTypeDoor"
        case HMAccessoryCategoryTypeDoorLock: return "HMAccessoryCategoryTypeDoorLock"
        case HMAccessoryCategoryTypeFan: return "HMAccessoryCategoryTypeFan"
        case HMAccessoryCategoryTypeGarageDoorOpener: return "HMAccessoryCategoryTypeGarageDoorOpener"
        case HMAccessoryCategoryTypeIPCamera: return "HMAccessoryCategoryTypeIPCamera"
        case HMAccessoryCategoryTypeLightbulb: return "HMAccessoryCategoryTypeLightbulb"
        case HMAccessoryCategoryTypeOutlet: return "HMAccessoryCategoryTypeOutlet"
        case HMAccessoryCategoryTypeProgrammableSwitch: return "HMAccessoryCategoryTypeProgrammableSwitch"
        case HMAccessoryCategoryTypeRangeExtender: return "HMAccessoryCategoryTypeRangeExtender"
        case HMAccessoryCategoryTypeSensor: return "HMAccessoryCategoryTypeSensor"
        case HMAccessoryCategoryTypeSwitch: return "HMAccessoryCategoryTypeSwitch"
        case HMAccessoryCategoryTypeThermostat: return "HMAccessoryCategoryTypeThermostat"
        case HMAccessoryCategoryTypeVideoDoorbell: return "HMAccessoryCategoryTypeVideoDoorbell"
        case HMAccessoryCategoryTypeWindow: return "HMAccessoryCategoryTypeWindow"
        case HMAccessoryCategoryTypeWindowCovering: return "HMAccessoryCategoryTypeWindowCovering"
        case HMAccessoryCategoryTypeAirPurifier: return "HMAccessoryCategoryTypeAirPurifier"
        case HMAccessoryCategoryTypeAirHeater: return "HMAccessoryCategoryTypeAirHeater"
        case HMAccessoryCategoryTypeAirConditioner: return "HMAccessoryCategoryTypeAirConditioner"
        case HMAccessoryCategoryTypeAirHumidifier: return "HMAccessoryCategoryTypeAirHumidifier"
        case HMAccessoryCategoryTypeAirDehumidifier: return "HMAccessoryCategoryTypeAirDehumidifier"
        case HMAccessoryCategoryTypeSprinkler: return "HMAccessoryCategoryTypeSprinkler"
        case HMAccessoryCategoryTypeFaucet: return "HMAccessoryCategoryTypeFaucet"
        case HMAccessoryCategoryTypeShowerHead: return "HMAccessoryCategoryTypeShowerHead"
        default: return "HMAccessoryCategoryTypeUnknown"
        }
    }
}

/*
See LICENSE folder for this sample’s licensing information.

Abstract:
A singleton for holding the home manager.
*/

import UIKit
import HomeKit


extension Notification.Name {
    static let homeDidUpdate = Notification.Name(rawValue: "APHKHomeDidUpdateNotification")
    static let accessoryDidUpdateName = Notification.Name(rawValue: "APHKAccessoryDidUpdateNameNotification")
}

/// - Tag: HomeStore
class HomeStore: NSObject {
    /// A singleton that can be used anywhere in the app to access the home manager.
    static let shared = HomeStore()
    
    static let LingDuManufacture = "Shenzhen Lingdu Auto Electronics Co., Ltd."
    /// The one and only home manager that belongs to the home store singleton.
    let homeManager = HMHomeManager()
    
    /// A set of objects that want to receive home delegate callbacks.
    var homeDelegates = Set<NSObject>()
    
    /// A set of objects that want to receive accessory delegate callbacks.
    var accessoryDelegates = Set<NSObject>()
}

// Actions performed by a given client that change HomeKit state don't generate
//  delegate callbacks in the same client. These convenience methods each
//  perform a particular update and make the corresponding delegate call.
extension HomeStore {
    
    /// Updates the name of a service and informs all accessory delegates.
    func updateService(_ service: HMService, name: String) {
        service.updateName(name) { error in
            if let error = error {
                print(error)
            } else if let accessory = service.accessory {
                self.accessory(accessory, didUpdateNameFor: service)
            }
        }
    }
    
    /// Moves an accessory to a given room and informs all the home delegates.
    func move(_ accessory: HMAccessory, in home: HMHome, to room: HMRoom) {
        home.assignAccessory(accessory, to: room) { error in
            if let error = error {
                print(error)
            } else {
                self.home(home, didUpdate: room, for: accessory)
            }
        }
    }
    
    /// Removes an accessory from a home and informs all the home delegates.
    func remove(_ accessory: HMAccessory, from home: HMHome) {
        home.removeAccessory(accessory) { error in
            if let error = error {
                print(error)
            } else {
                self.home(home, didRemove: accessory)
            }
        }
    }
}

/*
See LICENSE folder for this sample’s licensing information.

Abstract:
A central hub for accessory delegates.
*/

import HomeKit

extension HomeStore {
    /// Registers an object as an accessory delegate.
    func addAccessoryDelegate(_ delegate: NSObject) {
        accessoryDelegates.insert(delegate)
    }
    
    /// Deregisters a particular accessory delegate.
    func removeAccessoryDelegate(_ delegate: NSObject) {
        accessoryDelegates.remove(delegate)
    }
    
    /// Deregisters all accessory delegates.
    func removeAllAccessoryDelegates() {
        accessoryDelegates.removeAll()
    }
}

extension HomeStore: HMAccessoryDelegate {
    
    // The home store's only interest in the accessory updates is distributing them
    //  to the objects that have registered as accessory delegates. Each of these
    //  methods therefore simply passes along the call to all the items in the set,
    //  after first ensuring that the item is in fact an accessory delegate.
    
    func accessory(_ accessory: HMAccessory, didUpdateNameFor service: HMService) {
        print(#function)
        accessoryDelegates.forEach {
            guard let delegate = $0 as? HMAccessoryDelegate else { return }
            delegate.accessory?(accessory, didUpdateNameFor: service)
        }
    }
    
    func accessoryDidUpdateReachability(_ accessory: HMAccessory) {
        print(#function)
        accessoryDelegates.forEach {
            guard let delegate = $0 as? HMAccessoryDelegate else { return }
            delegate.accessoryDidUpdateReachability?(accessory)
        }
    }
    
    func accessoryDidUpdateServices(_ accessory: HMAccessory) {
        print(#function)
        accessoryDelegates.forEach {
            guard let delegate = $0 as? HMAccessoryDelegate else { return }
            delegate.accessoryDidUpdateServices?(accessory)
        }
    }
    
    func accessory(_ accessory: HMAccessory, service: HMService, didUpdateValueFor characteristic: HMCharacteristic) {
        print(#function)
        accessoryDelegates.forEach {
            guard let delegate = $0 as? HMAccessoryDelegate else { return }
            delegate.accessory?(accessory, service: service, didUpdateValueFor: characteristic)
        }
    }
}


/*
See LICENSE folder for this sample’s licensing information.

Abstract:
A central hub for home delegates.
*/

import HomeKit

extension HomeStore {
    /// Registers an object as a home delegate.
    func addHomeDelegate(_ delegate: NSObject) {
        homeDelegates.insert(delegate)
    }
    
    /// Deregisters a particular home delegate.
    func removeHomeDelegate(_ delegate: NSObject) {
        homeDelegates.remove(delegate)
    }
    
    /// Deregisters all home delegates.
    func removeAllHomeDelegates() {
        homeDelegates.removeAll()
    }
}

extension HomeStore: HMHomeDelegate {
    
    // The home store's only interest in the home updates is distributing them
    //  to the objects that have registered as home delegates. Each of these
    //  methods therefore simply passes along the call to all the items in the set,
    //  after first ensuring that the item is in fact a home delegate.
    
    func homeDidUpdateName(_ home: HMHome) {
        print(#function)
        homeDelegates.forEach {
            guard let delegate = $0 as? HMHomeDelegate else { return }
            delegate.homeDidUpdateName?(home)
        }
    }

    func home(_ home: HMHome, didAdd accessory: HMAccessory) {
        print(#function)
        homeDelegates.forEach {
            guard let delegate = $0 as? HMHomeDelegate else { return }
            delegate.home?(home, didAdd: accessory)
        }
    }
    
    func home(_ home: HMHome, didUpdate room: HMRoom, for accessory: HMAccessory) {
        print(#function)
        homeDelegates.forEach {
            guard let delegate = $0 as? HMHomeDelegate else { return }
            delegate.home?(home, didUpdate: room, for: accessory)
        }
    }
    
    func home(_ home: HMHome, didRemove accessory: HMAccessory) {
        print(#function)
        homeDelegates.forEach {
            guard let delegate = $0 as? HMHomeDelegate else { return }
            delegate.home?(home, didRemove: accessory)
        }
    }
    
    func home(_ home: HMHome, didAdd room: HMRoom) {
        print(#function)
        homeDelegates.forEach {
            guard let delegate = $0 as? HMHomeDelegate else { return }
            delegate.home?(home, didAdd: room)
        }
    }
    
    func home(_ home: HMHome, didUpdateNameFor room: HMRoom) {
        print(#function)
        homeDelegates.forEach {
            guard let delegate = $0 as? HMHomeDelegate else { return }
            delegate.home?(home, didUpdateNameFor: room)
        }
    }

    func home(_ home: HMHome, didRemove room: HMRoom) {
        print(#function)
        homeDelegates.forEach {
            guard let delegate = $0 as? HMHomeDelegate else { return }
            delegate.home?(home, didRemove: room)
        }
    }

    func home(_ home: HMHome, didEncounterError error: Error, for accessory: HMAccessory) {
        print(#function)
        homeDelegates.forEach {
            guard let delegate = $0 as? HMHomeDelegate else { return }
            delegate.home?(home, didEncounterError: error, for: accessory)
        }
   }
}


//
//  FWIPCLiveView.swift
//  FuweiCloud
//
//  Created by michael on 2024/5/24.
//

import UIKit

class FWIPCIndicator: UIView { // 录屏提示控件
    private let dot = UIView(color: .error)
    private let label = UILabel(text: "00:00",font: 14.5, color: .label)
    convenience init() {
        self.init(frame: CGRectMake(0, 0, 65, 22))
        self.isHidden = true
        backgroundColor = UIColor.black.alpha(0.3)
        addCorner(11)
        dot.addCorner(7*0.5)
        addSubviews([dot,label])
        dot.snp.makeConstraints { make in
            make.left.equalTo(8)
            make.size.equalTo([7].size)
            make.centerY.equalToSuperview()
        }
        label.snp.makeConstraints { make in
            make.left.equalTo(dot.snp.right).offset(4)
            make.centerY.equalToSuperview()
        }
    }
    
    var durarion: Int = 0 {
        didSet {
            label.text = String(format: "%02d:%02d",durarion/60, durarion%60)
        }
    }
    
    var isRecord: Bool = true {
        didSet {
            self.isHidden = !isRecord
            if isHidden {
                durarion = 0
            }
        }
    }
}

// MARK: - FWIPCFloatTipView
class FWIPCFloatTipView: UIView { // 悬浮提示控件：进入相册
    private var imgV: UIImageView!
    private var label = UILabel(text: "", font: 14.5, color: .label, lines: 2)
    private var intoPicBtn: UIButton!
    var eventBlock: (() -> Void)?
    private var timer:YYTimer!
    
    var tipViewHide: Bool = true {
        didSet {
            if !tipViewHide {
                if timer != nil {
                    self.isHidden = true
                    timer.invalidate()
                }
                dispatch_after_on_main_queue(0.1) { // 是否需要重复出现
                    self.isHidden = false
                }
                timer = YYTimer(timeInterval: 3, target: self, selector: APSEL("timeoutAction"), repeats: false)
            } else {
                self.isHidden = true
            }
        }
    }

    
    convenience init() {
        self.init(frame: .zero)
        self.backgroundColor = APHEX("#1C1C1E")
        self.isHidden = true
        addCorner(8)
        
        imgV = UIImageView()
        imgV.addCorner(6)
        intoPicBtn = UIButton.apGradientButton(AString("Enter photo album".local, font: 12.5, color: UIColor.label), action: .buttonAction, target: self)
        intoPicBtn.addCorner(29*0.5)
        addSubviews([imgV,label,intoPicBtn])
        
        imgV.snp.makeConstraints { make in
            make.left.equalTo(12)
            make.centerY.equalToSuperview()
            make.size.equalTo([70,39].size)
        }
        intoPicBtn.snp.makeConstraints { make in
            make.right.equalTo(-12)
            make.centerY.equalToSuperview()
            make.size.equalTo([72,29].size)
        }
        label.snp.makeConstraints { make in
            make.left.equalTo(imgV.snp.right).offset(9)
            make.right.equalTo(intoPicBtn.snp.left)
            make.centerY.equalToSuperview()
        }
    }
    
    func showTipView(_ filePath:String,_ tip:String, eventBlock:@escaping () -> Void) {
        imgV.sd_setImage(with: NSURL(fileURLWithPath: filePath) as URL, placeholderImage: nil, options: .retryFailed)
        label.text = tip.local
        tipViewHide = false
        self.superview?.bringSubviewToFront(self) // 防止被遮挡
        self.eventBlock = eventBlock
    }
    
    @objc private func buttonAction(_ btn:UIButton) {
        eventBlock?()
    }
    
    @objc func timeoutAction() {
        self.isHidden = true
        if (timer != nil) {
            timer.invalidate()
            timer = nil;
        }
    }
    
    deinit {
        //timeoutAction()
    }
}

class APQuickReplyPopView: APPopView { // 快捷回复
    var tableView:APBaseTableView<Any>!
    var dataSource:[String]!
    //var selectedIndex:NSInteger = 0
    override func initVariable() {
        super.initVariable()
        self.type = 1
        self.contentWidth = APScreenW - 32
        contentBottom = APSafeBottomHeight
        self.contentHeight = 235
        
//        if let userInfo = self.userInfo as? NSDictionary,
//           let dataSource = userInfo["dataSource"] as? [String],
//           let selectedIndex = userInfo["selectedIndex"] as? NSInteger {
//            self.selectedIndex = APClamp(selectedIndex, 0, dataSource.count-1)
//            self.dataSource = dataSource
//        }
        self.dataSource = ["Please stay away from my house".local,"Please leave the courier at the door, thank you".local,"I'm sorry, I can't open the door right now".local]
    }
    
    override func initContentUI() {
        let titleLabel = UILabel(text: "Quick reply".local, font: 18.6, color: .label,aligment: .center)
        let tableView = APBaseTableView<String>(cells: [APreplyCell.self], style: .plain, separator: .singleLine)
        tableView.separatorInset = UIEdgeInsets(top: 0, left: 12, bottom: 0, right: 12)
        let line = UIView(color: UIColor.apSeperatorColor_37)
        contentView.addSubviews([titleLabel,line,tableView])
        titleLabel.snp.makeConstraints { make in
            make.left.equalTo(12)
            make.right.equalTo(-12)
            make.top.equalTo(15)
        }
        line.snp.makeConstraints { make in
            make.left.equalTo(12)
            make.right.equalTo(-12)
            make.height.equalTo(1)
            make.top.equalTo(titleLabel.snp.bottom).offset(15)
        }
        tableView.snp.makeConstraints { make in
            make.left.right.equalTo(0)
            make.top.equalTo(titleLabel.snp.bottom).offset(15)
            make.bottom.equalTo(-17)
        }
        tableView.heightForRows = { _,_,_ in UITableView.automaticDimension }
        tableView.cellForRows = { _,r,model in
            let cell = tableView.dequeueReusableCell(named: APreplyCell.self)
            cell.titleLabel.text = model
            return cell
        }
        tableView.didSelectRow = {[unowned self] _,r,model in
//            if selectedIndex != r {
//                selectedIndex = r
//                tableView.reloadData()
//            }
            self.dismiss(withUserInfo: r)
        }
        tableView.dataArray = dataSource
        
        layoutIfNeeded()
        contentView.snp.updateConstraints { make in
            make.height.equalTo(tableView.contentSize.height+50)
        }
    }
    class APreplyCell: APBaseTableViewCell<[String]> {
        let titleLabel = UILabel(text: "",font: 14.6,color: .label,lines:0,aligment: .center)
        override func initContent() {
            self.backgroundColor = .clear
            contentView.addSubviews([titleLabel])
            titleLabel.snp.makeConstraints { make in
                make.left.equalTo(10)
                make.right.equalTo(-10)
                make.top.equalTo(18)
                make.bottom.equalTo(-18)
            }
        }
    }
}




// MARK: - FWIPCLiveView
class FWIPCLiveView: UIView {
    var remoteCall = false {
        didSet {
            hangupButton.isHidden = !remoteCall
            unlockView.snp.remakeConstraints { make in
                if remoteCall {
                    make.right.equalTo(-40)
                }else {
                    make.centerX.equalToSuperview()
                }
                make.bottom.equalTo(hangupButton)
                make.size.equalTo([170, 64].size)
            }
        }
    }
    var eventBlock: ((UIButton?, APIPCPanelType) -> Void)? = nil
    let bitrateLabel = UILabel(font: 13, color: .white) // 播放速率
    let playerView = UIView()
    let batteryView = APBatteryView(height: 20)
    var liveState: APRtcLiveState = .offline {
        didSet {
            if liveState == .streaming {
                sendSubviewToBack(liveView)
            }
            let allowTypes: [APIPCPanelType] = [.close,.more,.album,.playBack]
            let subviews = sectionView1.subviews + sectionView2.subviews + sectionView3.subviews
            subviews.forEach { subview in
                if let type = APIPCPanelType(rawValue: subview.tag) {
                    let isEnabled = (liveState == .streaming) || allowTypes.contains(type)
                    viewEnabled(subview, isEnabled)
                }
            }
            
            // 单独处理的按钮(非subviews)
            [resolutionButton,fullscreenButton].forEach { view in // 隐藏 ,unlockView
                if !notInFunctionIDViews.contains(view) {
                    if liveState == .streaming {
                        view.isHidden = shouldHidden ? shouldHidden:false // 隐藏
                        //viewEnabled(view, true) // 暗
                    }else{
                        view.isHidden = true
                        //viewEnabled(view, false)
                    }
                }
            }
        }
    }
    var recordDuration: Int = 0 {
        didSet {
            recordIndicator.durarion = recordDuration
        }
    }
    var isRecording: Bool = false {
        didSet {
            recordIndicator.isRecord = isRecording
        }
    }
    
    var voiceChanger: Int = 0 {
        didSet {
            if let btn = functionButtons.first(where: { APIPCPanelType(rawValue: $0.tag) == .voiceChanger }) {
                let images = ["vc_none_ic","vc_loli_ic","vc_uncle_ic"]
                btn.setImage(UIImage(named: images[voiceChanger]))
            }
        }
    }
//    var isLandscape:Bool = isLandscape
    
    private var statusBarHeight: CGFloat { isLandscape ? 25:APStatusBarHeight }
    private var btnGap: CGFloat { isLandscape ? 12:0 }
    private var btnMargin: CGFloat { // 按钮间距
        guard functionButtons != nil else { return 0 }
        let count = CGFloat(self.functionButtons.count)
        return if isLandscape {
            (APScreenH - ((btnSize.height+btnGap)*count - btnGap)) / 2
        }else {
            (APScreenW - btnSize.width*count) / 2
        }
    }
    private let leftPD: CGFloat = 20 // 左右两边的间距
    private var btnSize: CGSize { // 按钮大小
        isLandscape ?CGSizeMake(44, 44):CGSizeMake((APScreenW - self.leftPD*2)/5, 70)
    }
    
    private lazy var liveView: FWLiveScrollView = {
        FWLiveScrollView(frame: CGRectMake(0, (APScreenH-APScreenW/device.whRatio())*0.5, APScreenW, APScreenW/device.whRatio()), zoomView: playerView, aspectRatio: device.whRatio(), fullscreen: false)
    }()
    
    private let titleLabel = UILabel(font: 17, color: .label)
    private let backButton = UIButton(image: "page_return_ic")
    private let settingButton = UIButton(image: "ipc_setting_ic")
    private let topBGImgView: UIImageView = {
        let imgV = UIImageView(named: "ipc_topBackground")
        imgV.alpha = 0.5
        return imgV
    }()
    let recordIndicator = FWIPCIndicator()
    private let resolutionButton = UIButton(title: AString("HD".local, font: 13.5, color: .label)) // 分辨率
    private let fullscreenButton = UIButton(image: "dashboard_fullscreen_ic")
    private let voiceChangerButton = UIButton(image: "vc_none_ic")
    private var functionIDs: [APIPCPanelType]!
    private var functionButtons: [UIButton]! // sectionView1按钮 间距计算
    private var panels = [APIPCPanel]()
    private var device: APDeviceModel!
    private var shouldHidden: Bool = false
    private var notInFunctionIDViews = [UIView]() //后台可配置控件(未配置) 已隐藏，不参与点按显隐的控件
    private lazy var landscapeHideViews = [settingButton,fullscreenButton,unlockView] // 可配置控件,横屏隐藏
    
    private var sectionView1 = UIView() // 竖屏底部第1组
    private var sectionView2: UIView = { // 竖屏底部第2组
        let view = UIView()
        view.isHidden = true
        return view
    }()
    private var sectionView3: UIView = { // 第3组 横屏底部组
        let view = UIView()
        view.isHidden = true
        return view
    }()
    private var sectionView1Hidden: Bool = false { // 取记录的状态
        didSet {
            sectionView1.isHidden = sectionView1Hidden
        }
    }
    private var sectionView2Hidden: Bool = true {
        didSet {
            sectionView2.isHidden = sectionView2Hidden
        }
    }
    private var sectionView3Hidden: Bool = false {
        didSet {
            sectionView3.isHidden = sectionView3Hidden
        }
    }
    
    private let hangupButton = UIButton(image: nil, bImage: "live_hangup_ic") // 挂断
    let unlockView = FWLiveSwipeUnlockView(frame: CGRectMake(0, 0, 170, 64)) // 开锁
    
    private var lastSoundBtnState:Bool? // soundBtn
    
    private var talkGifImgV: YYAnimatedImageView = {
        let imgV = YYAnimatedImageView(image: YYImage.init(named: "ipc_talkGif"))
        imgV.isHidden = true
        return imgV
    }()
    
    var closeAlarmBlock: (() -> Void)? = nil
    
    var floatTipView = FWIPCFloatTipView() // 悬浮提示控件

    
    // MARK: - init
    convenience init(device: APDeviceModel) {
        self.init()
        self.device = device
        setupUI()
        eventHandle()
    }
    
    private func setupUI() {
        titleLabel.text = device.device_name
        titleLabel.textAlignment = .center
        titleLabel.numberOfLines = 0
        batteryView.battery = APBattery(value: device.power, status: device.power_status)
        batteryView.isHidden = true
        hangupButton.isHidden = true
        hangupButton.addEventHandler { [unowned self] _ in
            backButton.sendActions(for: .touchUpInside)
        }
        resolutionButton.contentEdgeInsets = UIEdgeInsets(horizontal: 15, vertical: 0)
        resolutionButton.addCorner(11, borderWidth: 1, borderColor: .label)
        
        
        // 后台数据 + 固定项
        let fixIDs:[APIPCPanelType] = [.returnBack,.title] // 固定项
        functionIDs = device.ipc_panels().map(\.id)
        functionIDs.forEach{print("function id:\($0)")}
        functionIDs += fixIDs // 总
        let isAutoBitrate = functionIDs.contains(where: { $0 == .autoBitrate })
        [APIPCPanelType.preview:liveView,
         APIPCPanelType.title: titleLabel,
         APIPCPanelType.returnBack: backButton,
         APIPCPanelType.battery: batteryView,
         APIPCPanelType.setting: settingButton,
         isAutoBitrate ? APIPCPanelType.autoBitrate:APIPCPanelType.resolution:resolutionButton,
         APIPCPanelType.bitrate:bitrateLabel,
         APIPCPanelType.fullscreen:fullscreenButton,
         APIPCPanelType.swipeUnlock:unlockView,
        ].forEach { (type, view) in
            addSubview(view)
            view.tag = Int(type.rawValue)
            view.isHidden = !functionIDs.contains(type) // 后台数据过滤
            if type == .battery { // 已隐藏
                view.isHidden = true
            }
            if view.isHidden { notInFunctionIDViews.append(view) }
            if let btn = view as? UIButton { // 底部按钮事件
                btn.addAction(.buttonAction, onTarget: self)
            }
        }
        
        notInFunctionIDViews += [liveView,floatTipView] // 不参与点按显隐的控件
        
        // 放置到上层
        addSubviews([topBGImgView,backButton,titleLabel,settingButton,recordIndicator,hangupButton])
        
        // 添加底部view: 竖屏sectionView1、sectionView2  横屏sectionView3
        addBottomViews()
        
        // gif TipView
        addSubviews([talkGifImgV,floatTipView])
        
        settingButton.snp.makeConstraints { make in
            make.right.equalTo(-20)
            make.top.equalTo(statusBarHeight)
            make.size.equalTo([44].size)
        }
        //subviews.forEach { $0.addShadowColor(.black, radius: 3, offset: .zero) }
        
        updateSubviewsLayout()
    }
    
    
    // MARK: - event
    private func eventHandle() {
        
        liveView.clickBlock = { [unowned self] in // 屏幕单击  显示隐藏控件  **新增控件需要判断
            if liveState != .streaming { return }
            shouldHidden.toggle()
            subviews.forEach { v in
                // functionIDs控件
                if functionIDs.contains(APIPCPanelType(rawValue: v.tag)!) && !notInFunctionIDViews.contains(v) {
                    if (isLandscape && !landscapeHideViews.contains(v)) || !isLandscape  {
                        v.isHidden = shouldHidden
                    }
                }
                
                if v == hangupButton {
                    v.isHidden = (remoteCall && !isLandscape) ? shouldHidden : true
                }
                if v == recordIndicator {
                    v.isHidden = isRecording ? shouldHidden : true
                }
                if v == talkGifImgV {
                    if let talkBtn = button(of: .talk), talkBtn.isSelected, APGlobal.talkType==1 {
                        talkGifImgV.isHidden = shouldHidden
                    } else {
                        talkGifImgV.isHidden = true
                    }
                }
            }
            
            
            resolutionButton.isHidden = shouldHidden // resolutionButton
            
            if isLandscape { // sectionView
                sectionView1.isHidden = true
                sectionView2.isHidden = true
                sectionView3.isHidden = shouldHidden ? shouldHidden:sectionView3Hidden
            }else{
                sectionView1.isHidden = shouldHidden ? shouldHidden:sectionView1Hidden // 取记录的状态
                sectionView2.isHidden = shouldHidden ? shouldHidden:sectionView2Hidden
                sectionView3.isHidden = true
            }
        }
        
        liveView.doubleClickBlock = { [unowned self] in  // 屏幕双击 全屏切换
            if liveState != .streaming { return }
            if functionIDs.contains(.fullscreen) {
                FWFullscreenManager.switching(viewController)
            }
        }
        
        //viewEnabled(unlockView, FWMQTTManager.shared.isConnected)
        addTap { [unowned self] _, _, _ in // view点击
            liveView.clickBlock?()
        }
        
        unlockView.eventBlock = { [weak self] in
            guard let self else { return }
            eventBlock?(nil, .swipeUnlock)
        }
        
        // Notification
        addNotification(withName: .talkTypeChanged, object: nil) { [weak self] note in
            self?.updateTalkBtn()
        }
    }
    
    
    private func viewEnabled(_ view: UIView, _ isEnabled: Bool) {
        view.alpha = isEnabled ? 1 : 0.4
        view.isUserInteractionEnabled = isEnabled
    }
    
    func addBottomViews() {
        addSubviews([sectionView1,sectionView2,sectionView3])
        sectionView1.snp.makeConstraints { make in
            make.left.right.equalToSuperview()
            make.height.equalTo(btnSize.height)
            make.bottom.equalToSuperview().offset(-APSafeBottomHeight-5)
        }
        sectionView2.snp.makeConstraints { make in
            make.edges.equalTo(sectionView1)
        }
        sectionView3.snp.makeConstraints { make in
            make.left.right.equalToSuperview()
            make.height.equalTo(58)
            make.bottom.equalToSuperview().offset(-APSafeBottomHeight-4)
        }
        setSectionView12()
        setSetionView3()
    }
    
    // 设置竖屏底部按钮 1、2组
    private func setSectionView12() {
        if sectionView1.subviews.count > 0 {
            sectionView1.removeAllSubviews()
            sectionView2.removeSubviews()
            panels.removeAll()
        }
        
        // 取ipc_pannels数组的最后一个
        guard let bottoms = device.function_setting.ipc_pannels.last?.bottom else { return }
        var functionPanels = bottoms.map({APIPCPanel(id: APIPCPanelType(rawValue: $0 as! Int)!)})
        functionPanels.removeFirst(where: { $0.id == .preview}) // 移除 preview
        var subPanels1 = [APIPCPanel]()
        var subPanels2 = [APIPCPanel]()
        if functionPanels.count > 5 {
            subPanels1 = Array(functionPanels[0...3])
            subPanels1.append(APIPCPanel(id: .more))
            
            subPanels2 = Array(functionPanels[4...])
            let count = 5-subPanels2.count
            for index in 0 ..< count {
                if index == count-1 {
                    subPanels2.append(APIPCPanel(id: .close))
                } else {
                    subPanels2.append(APIPCPanel(id: .unknown))
                }
            }
            addSectionView12Btns(sectionView1,subPanels1)
            addSectionView12Btns(sectionView2,subPanels2)
        }else{
            subPanels1 = functionPanels
            addSectionView12Btns(sectionView1,subPanels1)
        }
        
        upSectionButtons(panels,false)
    }
    
    private func addSectionView12Btns(_ view: UIView, _ subPanels:[APIPCPanel]) {
        let functionButtons1 = subPanels.map { panel in
            panels.append(panel)
            let btn = UIButton()
            btn.tag = panel.id.rawValue
            btn.addAction(.buttonAction, onTarget: self)
            view.addSubview(btn)
            return btn
        }
        functionButtons = functionButtons1
        for (idx, btn) in functionButtons1.enumerated() {
            btn.snp.remakeConstraints { make in
                if idx == 0 {
                    make.left.equalTo(btnMargin)
                }else {
                    make.left.equalTo(functionButtons1[idx - 1].snp.right).offset(btnGap)
                }
                make.bottom.equalTo(0)
                make.size.equalTo(btnSize)
            }
        }
    }

    private var sec3LastBtn = UIButton()
    private var panels3 = [APIPCPanel]()
    private let baseTag = 1000
    
    private func setSetionView3() {
        let subPanels3 = [APIPCPanelType]([.snapshot,.record,.alarm,.sound,.talk]) //.resolution,
            .map { APIPCPanel(id: $0) }
        let functionButtons3 = subPanels3.map { panel in
            panels3.append(panel)
            let btn = UIButton()
            btn.tag = panel.id.rawValue + baseTag
            btn.addTarget(self, action: #selector(buttonAction3), for: .touchUpInside)
            sectionView3.addSubview(btn)
            return btn
        }
        
        for (idx, btn) in functionButtons3.enumerated() {
            btn.snp.remakeConstraints { make in
                if idx == functionButtons3.count-1 {
                    make.right.equalTo(-28)
                    make.size.equalTo([58].size)
                }else{
                    if idx == 0 {
                        make.left.equalTo(btnMargin>0 ? btnMargin:28)
                    }else {
                        make.left.equalTo(functionButtons3[idx - 1].snp.right).offset(btnGap)
                    }
                    make.size.equalTo(btnSize)
                }
                make.centerY.equalToSuperview()
            }
            if idx == functionButtons3.count-2 { sec3LastBtn = btn }
        }
    
        upSectionButtons(panels3,true)
    }

    
    // 更新按钮图标
    private func upSectionButtons(_ panelsT:[APIPCPanel], _ isSection3: Bool) {
        panelsT.forEach { panel in
            var btn = UIButton()
            if panel.id != 0 {
                if isSection3 {
                    btn = sectionView3.viewWithTag(panel.id.rawValue+baseTag) as! UIButton
                }else {
                    btn = viewWithTag(panel.id.rawValue) as! UIButton
                }
            }
            
            btn.setTitle("", image: panel.imageName())
            if let imageName = panel.select()?.imageName() {
                btn.setTitle("", image: imageName, state: 4)
            }
            if panel.id == APIPCPanelType.voiceChanger.rawValue { /// 设备变声器图标
                let images = ["vc_none_ic","vc_loli_ic","vc_uncle_ic"]
                btn.setTitle("", image: UIImage(named: images[device.localConfig.voiceChanger]))
            }

            addLongPressGesture(panel, btn) // 长按
            addAlarmCountdownLB(panel,btn,isSection3) //告警
        }
    }
    
    // 重置所有按钮为初始状态
    func resetSectionBtns() {
        talkGifImgV.isHidden = true
        bitrateLabel.text = ""
        closeAlarmCountdown() // 关闭报警
        resolutionButton.setTitler(AString("HD".local, font: 13.5, color: .label)) // 恢复高清
        let subviews = sectionView1.subviews + sectionView2.subviews + sectionView3.subviews
        subviews.forEach { subview in
            if let btn = subview as? UIButton {
                if btn.isSelected {
                    let type = APIPCPanelType(rawValue: btn.tag) // 关闭扬声器 语音
                    if (type == .sound || type == .talk) && !sectionView3.subviews.contains(btn) {
                        eventBlock?(btn,type!)
                    }
                    btn.isSelected = false
                }
            }
        }
        //print("=resetSectionBtns")
    }
    
    // 根据类型获取按钮
    func button(of type: APIPCPanelType, isSecView3:Bool = false) -> UIButton? {
        if isSecView3 {
            sectionView3.viewWithTag(Int(type.rawValue)+baseTag) as? UIButton
        }else{
            viewWithTag(Int(type.rawValue)) as? UIButton
        }
    }
    
    // 添加告警倒计时
    var countLB:UILabel = { // 报警倒计时
        let lb = UILabel(text: "29s",font:12.5,color: .white,aligment: .center)
        lb.isHidden = true
        return lb
    }()
    var countLB3:UILabel = { // 报警倒计时
        let lb = UILabel(text: "29s",font:12.5,color: .white,aligment: .center)
        lb.isHidden = true
        return lb
    }()
    func addAlarmCountdownLB(_ panel:APIPCPanel ,_ btn: UIButton, _ isSection3:Bool) {
        if panel.id == APIPCPanelType.alarm {
            if isSection3==true {
                btn.addSubview(countLB3)
                countLB3.snp.makeConstraints { make in
                    make.left.right.equalTo(btn)
                    make.centerY.equalTo(btn).offset(8)
                    make.height.equalTo(15)
                }
            }else{
                btn.addSubview(countLB)
                countLB.snp.remakeConstraints { make in
                    make.left.right.equalTo(btn)
                    make.centerY.equalTo(btn).offset(8)
                    make.height.equalTo(15)
                }
            }
        }
    }
    
    
    // 添加长按
    func addLongPressGesture(_ panel: APIPCPanel, _ btn: UIButton) {
        if panel.id == APIPCPanelType.talk {
            if APGlobal.talkType==0 {
                let press = UILongPressGestureRecognizer{ [unowned self] press, state, location in
                    if liveState != .streaming { return }
                    if self.isRecording { APMakeToast("Be recording".local); return}
                    if state == .began {
                        if !isLandscape {
                            talkGifImgV.snp.remakeConstraints { make in
                                make.bottom.equalTo(sectionView1.snp.top).offset(-13)
                                make.centerX.equalTo(btn)
                                make.size.equalTo([80,20].size)
                            }
                        } else {
                            talkGifImgV.snp.remakeConstraints { make in
                                make.right.equalTo(btn.snp.left).offset(-18)
                                make.centerY.equalTo(btn)
                                make.size.equalTo([60,20].size)
                            }
                        }
                        
                        talkGifImgV.isHidden = false
                        eventBlock?(btn,.talk)
                        
                        if let soundBtn = button(of: .sound){ //单向对讲 关闭远程音频
                            lastSoundBtnState = soundBtn.isSelected
                            if soundBtn.isSelected {
                                eventBlock?(soundBtn,.sound)
                                button(of: .sound, isSecView3:true)!.isSelected.toggle()
                            }
                        }
                    }
                    if state == .ended {
                        talkGifImgV.isHidden = true
                        eventBlock?(btn,.talk)
                        
                        if let soundBtn = button(of: .sound), lastSoundBtnState == true { // 复原
                            eventBlock?(soundBtn,.sound)
                            button(of: .sound, isSecView3:true)!.isSelected.toggle()
                        }
                    }
                }
                press.minimumPressDuration = 1
                press.cancelsTouchesInView = true
                btn.addGestureRecognizer(press)
            }else {
                btn.removeAllGestures()
            }
        }
    }
    
    // 更新talk按钮
    func updateTalkBtn(){
        let btn12 = button(of: .talk, isSecView3:false)
        let btn3 = button(of: .talk, isSecView3:true)
        if ((btn12?.isSelected==true || btn3?.isSelected==true) &&  APGlobal.talkType==0) {
            eventBlock?(btn12!,.talk)
            btn12?.isSelected = false
            btn3?.isSelected = false
            talkGifImgV.isHidden = true
        }
         
        if let panel1 = panels.filter({ $0.id == APIPCPanelType.talk.rawValue }).first {
            btn12?.tag = panel1.id.rawValue
            upSectionButtons([panel1], false)
        }
        if let panel3 = panels.filter({ $0.id == APIPCPanelType.talk.rawValue }).first {
            btn3?.tag = panel3.id.rawValue + baseTag
            upSectionButtons([panel3], true)
        }
    }
    

    
    // MARK: - 横竖屏切换
    func updateSubviewsLayout() { // _ landscape: Bool
        //Landscape = landscape
        
        if !shouldHidden { // 横屏隐藏，竖屏显示
            landscapeHideViews.forEach { view in
                if !notInFunctionIDViews.contains(view) {
                    view.isHidden = isLandscape
                }
            }
        }
        
        // hangupButton
        hangupButton.isHidden = (remoteCall && !isLandscape) ? shouldHidden : true
        
        // sectionView1、2、3 是否隐藏
        if isLandscape {
            sectionView1.isHidden = true
            sectionView2.isHidden = true
            sectionView3.isHidden = shouldHidden ? shouldHidden:sectionView3Hidden
        } else {
            sectionView1.isHidden = shouldHidden ? shouldHidden:sectionView1Hidden
            sectionView2.isHidden = shouldHidden ? shouldHidden:sectionView2Hidden
            sectionView3.isHidden = true
        }
        
        liveView.setZoomScale(1, animated: false) // 切换时复原
        
        let maxSize = max(APScreenW, APScreenH)
        let minSize = min(APScreenW, APScreenH)
        let screenW = isLandscape ? maxSize : minSize
        let screenH = isLandscape ? minSize : maxSize
        if isLandscape { // 横屏
            liveView.frame = CGRectMake(0, 0, screenW, screenH)
            liveView.contentSize = CGSize(width: screenW, height: screenH)
            backButton.snp.remakeConstraints { make in
                make.left.equalTo(20)
                make.top.equalTo(statusBarHeight)
                make.size.equalTo([44].size)
            }
            titleLabel.snp.remakeConstraints { make in
                make.left.equalTo(backButton.snp.right).offset(10)
                make.top.equalTo(backButton)
                make.height.equalTo(44)
            }
            batteryView.snp.remakeConstraints { make in
                make.left.equalTo(titleLabel.snp.right).offset(8)
                make.centerY.equalTo(titleLabel)
                make.height.equalTo(batteryView.batteryHeight)
                make.width.equalTo(batteryView.batteryWidth)
            }
            
            recordIndicator.snp.remakeConstraints { make in
                make.left.equalTo(titleLabel.snp.left)
                make.top.equalTo(titleLabel.snp.bottom)
                make.size.equalTo(CGSizeMake(65, 22))
            }
            
            if !sectionView3.subviews.contains(resolutionButton) {
                sectionView3.addSubview(resolutionButton)
            }
            resolutionButton.snp.remakeConstraints { make in
                make.left.equalTo(sec3LastBtn.snp.right).offset(btnGap)
                make.centerY.equalTo(sec3LastBtn)
                //make.height.equalTo(22)
                make.size.equalTo([42,22].size)
            }
            
            bitrateLabel.snp.remakeConstraints { make in
                make.centerY.equalTo(titleLabel)
                make.centerX.equalToSuperview()
            }
            floatTipView.snp.remakeConstraints { make in
                make.bottom.equalTo(sectionView3.snp.bottom).offset(0)
                make.centerX.equalTo(sectionView3)
                make.size.equalTo([470,63].size)
            }
            topBGImgView.snp.remakeConstraints { make in
                make.top.left.right.equalToSuperview()
                make.height.equalTo(statusBarHeight+44)//56
            }

        }else { // 竖屏
            liveView.frame = CGRectMake(0, 0, screenW, screenH-statusBarHeight) // 上移
            liveView.contentSize = CGSize(width: screenW, height: screenH-statusBarHeight)
            backButton.snp.remakeConstraints { make in
                make.left.equalTo(20)
                make.top.equalTo(statusBarHeight)
                make.size.equalTo([44].size)
            }
            titleLabel.snp.remakeConstraints { make in
                make.centerY.equalTo(backButton)
                make.left.equalTo(75)
                make.right.equalTo(-75)
            }
            batteryView.snp.remakeConstraints { make in
                make.top.equalTo(APNaviBarHeight)
                make.centerX.equalToSuperview()
                make.height.equalTo(batteryView.batteryHeight)
                make.width.equalTo(batteryView.batteryWidth)
            }
            
            if !subviews.contains(resolutionButton) {
                addSubview(resolutionButton)
            }
            
            hangupButton.snp.remakeConstraints { make in
                make.left.equalTo(40)
                make.bottom.equalTo(sectionView1.snp.top).offset(-124) // -90
                make.size.equalTo([64].size)
            }
            
            unlockView.snp.remakeConstraints { make in
                if remoteCall {
                    make.right.equalTo(-40)
                }else {
                    make.centerX.equalToSuperview()
                }
                make.bottom.equalTo(hangupButton)
                make.size.equalTo([170, 64].size)
            }
            
            // resolutionButton
            if APIPCPanelType(rawValue: resolutionButton.tag) == .resolution {
                resolutionButton.snp.remakeConstraints { make in
                    make.left.equalTo(16)
                    make.bottom.equalTo(sectionView1.snp.top).offset(-50) // liveView
                    make.size.equalTo([42,22].size)
                }
            }else{ // 自适应
                resolutionButton.snp.remakeConstraints { make in
                    make.right.equalTo(-20)
                    make.top.equalTo(topBGImgView.snp.bottom).offset(30)
                    make.size.equalTo([42,22].size)
                }
            }
            
            bitrateLabel.snp.remakeConstraints { make in
                make.top.equalTo(backButton.snp.bottom).offset(10)
                make.centerX.equalToSuperview()
            }
            fullscreenButton.snp.remakeConstraints { make in
                make.right.equalTo(-20)
                make.bottom.equalTo(sectionView1.snp.top).offset(-50)
                make.size.equalTo([44].size)
            }
            floatTipView.snp.remakeConstraints { make in
                make.bottom.equalTo(sectionView1.snp.top).offset(-35)
                make.centerX.equalTo(sectionView1)
                make.size.equalTo([343,63].size)
            }
            topBGImgView.snp.remakeConstraints { make in
                make.top.left.right.equalToSuperview()
                make.height.equalTo(statusBarHeight+44) //88
            }
        }
        
        //talkGifImgV
        if !isLandscape {
            guard let talkBtn = button(of: .talk, isSecView3: false) else { return }
            talkGifImgV.snp.remakeConstraints { make in
                make.bottom.equalTo(sectionView1.snp.top).offset(-13)
                make.centerX.equalTo(talkBtn)
                make.size.equalTo([80,20].size)
            }
        } else {
            guard let talkBtn = button(of: .talk, isSecView3: true) else { return }
            talkGifImgV.snp.remakeConstraints { make in
                make.right.equalTo(talkBtn.snp.left).offset(-18)
                make.centerY.equalTo(talkBtn)
                make.size.equalTo([60,20].size)
            }
        }
        
        // recordIndicator
        if !isLandscape {
            guard let recordBtn = button(of: .record, isSecView3: false) else { return }
            recordIndicator.snp.remakeConstraints { make in
                make.bottom.equalTo(sectionView1.snp.top).offset(-8)
                make.centerX.equalTo(recordBtn)
                make.size.equalTo(CGSizeMake(65, 22))
            }
        } else {
            guard let recordBtn = button(of: .record, isSecView3: true) else { return }
            recordIndicator.snp.remakeConstraints { make in
                make.bottom.equalTo(sectionView3.snp.top).offset(-8)
                make.centerX.equalTo(recordBtn)
                make.size.equalTo(CGSizeMake(65, 22))
            }
        }
    }
    
    
    // MARK: - 按钮事件
    //第1、2组
    @objc private func buttonAction(_ btn: UIButton) {
        guard let type = APIPCPanelType(rawValue: btn.tag) else { return }
        buttonActions(btn,type,false)
    }
    //第3组
    @objc private func buttonAction3(_ btn: UIButton) {
        guard let type = APIPCPanelType(rawValue: btn.tag-baseTag) else { return }
        buttonActions(btn,type,true)
    }
    //buttons 按钮事件处理
    func buttonActions(_ btn:UIButton,_ type:APIPCPanelType,_ isSection3:Bool) {
        if type == .more {
            sectionView1Hidden = true
            sectionView2Hidden = false
            return
        }
        if type == .close {
            sectionView1Hidden = false
            sectionView2Hidden = true
            return
        }
        
        if type == .talk || type == .resolution || type == .album  || type == .playBack || type == .setting  || type == .sound || type == .returnBack {
            if self.isRecording { APMakeToast("Be recording".local); return} // 正在录制
        }
        
        
        if type == .talk {
            let status = AVCaptureDevice.authorizationStatus(for: .audio)
            guard status == .authorized else {
                APConfirmPopView.pop(withUserInfo: [
                    APPopProperty.header: "Please turn on your phone's microphone permission first".local,
                    APPopProperty.rightButton: "Setting".local,
                ]) { userInfo in
                    if nil == userInfo { return }
                    APAppSettingURLHelper.gotoURL(.appSetting)
                }
                return
            }
            
            if APGlobal.talkType == 0 { // 单向
                APMakeToast("Hold to speak".local)
                return
            } else { // 双向
                talkGifImgV.isHidden = btn.isSelected ? true:false
                APMakeToast(!btn.isSelected ? "Call start".local:"Call end".local)
                
                if !btn.isSelected,let soundBtn = button(of: .sound){ // 双向对讲 打开音频
                    lastSoundBtnState = soundBtn.isSelected
                    if !soundBtn.isSelected {
                        eventBlock?(soundBtn,.sound)
                        button(of: .sound, isSecView3:true)!.isSelected.toggle()
                    }
                }else{
                    if let soundBtn = button(of: .sound), lastSoundBtnState != soundBtn.isSelected { // 复原
                        eventBlock?(soundBtn,.sound)
                        button(of: .sound, isSecView3:true)!.isSelected.toggle()
                    }
                }
            }
        }
        
        if type == .sound || type == .soundOn {
            if self.isRecording { APMakeToast("Be recording".local); return} // 录制
            !btn.isSelected ? APMakeToast("Turned on sound".local):APMakeToast("Turned off sound".local)
        }
        
        // 关联按钮状态相互绑定
        if type == .record || type == .talk && (APGlobal.talkType == 1) || type == .sound { // .alarm：单独控制
            let anotherBtn = button(of: type,isSecView3:!isSection3) // 取另一个按钮
            anotherBtn?.isSelected.toggle()
        }
        
        eventBlock?(btn,type)
    }
    
    
    // 开启告警倒计时
    func startAlarmCountdown() {
        let btn1 = button(of: .alarm, isSecView3: false)!
        let btn3 = button(of: .alarm, isSecView3: true)!
        
        if !btn1.isSelected { // 如果打开状态
            btn1.isSelected.toggle()
        }
        if !btn3.isSelected {
            btn3.isSelected.toggle()
        }
        
        let timeOut = 30
        YYTimer.start(fireTime: 0, repeatCount: timeOut) { [unowned self] count in
            countLB.isHidden = false
            countLB3.isHidden = false
            countLB.text = "\(timeOut-1-count)s"
            countLB3.text = "\(timeOut-1-count)s"
            if count==timeOut-1 {
                closeAlarmCountdown() //关闭按钮
            }
        }
    }
    
    // 关闭告警倒计时
    func closeAlarmCountdown() {
        YYTimer.stop()
        guard let btn1 = button(of: .alarm, isSecView3: false) else { return }
        let btn3 = button(of: .alarm, isSecView3: true)!
        
        if btn1.isSelected || btn3.isSelected {
            closeAlarmBlock?()
        }
        
        if btn1.isSelected { // 如果打开则关闭
            btn1.isSelected.toggle()
        }
        if btn3.isSelected {
            btn3.isSelected.toggle()
        }
        countLB.isHidden = true
        countLB3.isHidden = true
    }
    
    // 更新白光灯
    func updateLightSwt(_ value:Int) {
        let btn = button(of: .light)
        btn?.isSelected = value==1 ? true:false
    }
    
    // 更新ResolutionBtn
    func updateResolutionBtn(_ title:String) {
        let btnW = title.width(13.5)+15
        resolutionButton.setTitler(AString(title, font: 13.5, color: .label))
        resolutionButton.snp.updateConstraints { make in
            make.size.equalTo([btnW,22].size)
        }
    }
    
    
    deinit {
        //recordIndicator.removeAllBlockObservers()
        guard let functionButtons else { return }
        functionButtons.forEach { $0.removeAllBlockObservers() }
    }
}

//
//  FWLiveScrollView.swift
//  FuweiCloud
//
//  Created by michael on 2024/5/24.
//

import UIKit

class FWLiveScrollView: UIScrollView, UIScrollViewDelegate {

    @objc var clickBlock: APVoidBlock?
    @objc var doubleClickBlock: APVoidBlock?
    private var lastFrame: CGRect? = nil
    private(set) var aspectRatio: CGFloat = 1
    private var fullscreen: Bool = false
    private var zoomView: UIView!
    private var firstFrame: CGRect?
    
    @objc
    convenience init(frame: CGRect, zoomView: UIView, aspectRatio: CGFloat, fullscreen: Bool) {
        self.init(frame: frame)
        self.fullscreen = fullscreen
        firstFrame = frame
        self.aspectRatio = aspectRatio
        clipsToBounds = true
        contentInsetAdjustmentBehavior = .never
        contentInset = .zero
        showsVerticalScrollIndicator = false
        showsHorizontalScrollIndicator = false
        minimumZoomScale = 1
        let w = frame.width, h = frame.height
        self.zoomView = zoomView
        self.zoomView.frame = CGRectMake(0, (h-w/aspectRatio)/2, w, w/aspectRatio)
        addSubview(zoomView)
        self.delegate = self
        let tap = UITapGestureRecognizer {[unowned self] _, _, _ in
            clickBlock?()
        }
        let doubleTap = UITapGestureRecognizer {[unowned self] _, _, location in
            doubleClickBlock?()
        }
        tap.require(toFail: doubleTap)
        doubleTap.numberOfTapsRequired = 2
        self.addGestureRecognizers([tap,doubleTap])
    }
    
    var zoomCenterOffsetY: CGFloat = 0
    
    override func layoutSubviews() {
        guard lastFrame != frame else { return }
        lastFrame = frame
        let w = frame.width, h = frame.height
        
        if w / h >= aspectRatio {
            maximumZoomScale = w/h/aspectRatio
            zoomView.frame = CGRectMake((w - h*aspectRatio)/2, 0, h*aspectRatio, h) // h固定
        }else {
            maximumZoomScale = h*aspectRatio/w
            zoomView.frame = CGRectMake(0, (h-w/aspectRatio)/2, w, w/aspectRatio) // w固定
        }
        print("max scale:\(maximumZoomScale)")
        if fullscreen { // 全屏
            fullscreen.toggle()
            self.contentOffset = CGPointMake(0, 0);
            self.setZoomScale(self.maximumZoomScale, animated: true)
        }else{ // 小屏
            
            if isLandscape {

            } else {
                maximumZoomScale = 4
                zoomView.center = self.center
            }
            
        }
    }

    private func zoom(at point: CGPoint) {
        let o1 = frame.origin
        let o2 = zoomView.frame.origin
        var p = point
        p.x -= o1.x + o2.x
        p.y -= o1.y + o2.y
        if zoomScale > minimumZoomScale {
            setZoomScale(minimumZoomScale, animated: true)
        }else {
            let w = frame.width / maximumZoomScale
            let h = frame.height / maximumZoomScale
            zoom(to: CGRectMake(p.x - w/2, p.y - h/2, w, h), animated: true)
        }
    }
    
    //MARK: UIScrollViewDelegate
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        self.zoomView
    }
    
    func scrollViewDidZoom(_ scrollView: UIScrollView) {

        var r = zoomView.frame // 缩放过程中调整位置
        r.origin.x = 0
        r.origin.y = 0
        if fullscreen {
            if r.width < bounds.width {
                r.origin.x = (bounds.width - r.width) / 2
            }
            if r.height < bounds.height {
                r.origin.y = (bounds.height - r.height) / 2
            }
            zoomView.frame = r
            panGestureRecognizer.isEnabled = zoomScale > minimumZoomScale
            
        } else {
            if isLandscape {
                if r.width < bounds.width {
                    r.origin.x = (bounds.width - r.width) / 2
                }
                if r.height < bounds.height {
                    r.origin.y = (bounds.height - r.height) / 2
                }
                panGestureRecognizer.isEnabled = zoomScale > minimumZoomScale
            } else {
                //调整位置
                zoomView.center = centerOfScrollViewContent(scrollView: scrollView)
                panGestureRecognizer.isEnabled = zoomScale > minimumZoomScale && zoomScale<maximumZoomScale
            }
        }
    }
    
    func centerOfScrollViewContent(scrollView: UIScrollView) -> CGPoint {
        let offsetX = (scrollView.bounds.size.width > scrollView.contentSize.width) ? (scrollView.bounds.size.width - scrollView.contentSize.width) * 0.5 : 0;
        let offsetY = (scrollView.bounds.size.height > scrollView.contentSize.height) ? (scrollView.bounds.size.height - scrollView.contentSize.height) * 0.5 : 0;
        let actualCenter = CGPointMake(scrollView.contentSize.width * 0.5 + offsetX, scrollView.contentSize.height * 0.5 + offsetY);
        return actualCenter;
    }

}

//
//  FWAVKit.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/8.
//

import AVFoundation

class FWAVKit: NSObject {
    static func separateAudioAndVideo(from videoURL: URL, videoCompletion: @escaping (URL?, Error?) -> Void, audioCompletion: @escaping (URL?, Error?) -> Void) {
        let asset = AVAsset(url: videoURL)
        
        guard let exportSessionVideo = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality),
              let exportSessionAudio = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetAppleM4A) else {
            videoCompletion(nil, NSError(domain: "com.yourapp", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to create export session for video"]))
            audioCompletion(nil, NSError(domain: "com.yourapp", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to create export session for audio"]))
            return
        }
        
        exportSessionVideo.outputFileType = .mp4
        exportSessionAudio.outputFileType = .mp3
        
        let documentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
        let videoOutputURL = URL(fileURLWithPath: documentsPath).appendingPathComponent("outputVideo.mp4")
        let audioOutputURL = URL(fileURLWithPath: documentsPath).appendingPathComponent("outputAudio.m4a")
        
        exportSessionVideo.outputURL = videoOutputURL
        exportSessionAudio.outputURL = audioOutputURL
        
        let videoTrack = asset.tracks(withMediaType: .video).first
        let audioTrack = asset.tracks(withMediaType: .audio).first
        
        exportSessionVideo.timeRange = videoTrack?.timeRange ?? CMTimeRange(start: .zero, duration: asset.duration)
        exportSessionAudio.timeRange = audioTrack?.timeRange ?? CMTimeRange(start: .zero, duration: asset.duration)
        
        exportSessionVideo.exportAsynchronously {
            DispatchQueue.main.async {
                videoCompletion(videoOutputURL, exportSessionVideo.error)
            }
        }
        
        exportSessionAudio.exportAsynchronously {
            DispatchQueue.main.async {
                audioCompletion(audioOutputURL, exportSessionAudio.error)
            }
        }
    }
    
    static func separateAudioAndVideo1(from videoURL: URL, completion: @escaping (URL?, Error?) -> Void) {
        let asset = AVAsset(url: videoURL)
            
            let composition = AVMutableComposition()
            guard let audioTrack = asset.tracks(withMediaType: .audio).first else {
                completion(nil, NSError(domain: "com.yourapp", code: -1, userInfo: [NSLocalizedDescriptionKey: "Audio track not found"]))
                return
            }
            
            let audioCompositionTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid)
            
            do {
                try audioCompositionTrack?.insertTimeRange(audioTrack.timeRange, of: audioTrack, at: .zero)
            } catch {
                completion(nil, error)
                return
            }
            
            let audioOutputURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("outputAudio.mp3")
            
            let audioExportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetAppleM4A)
            audioExportSession?.outputFileType = AVFileType.mp3
            audioExportSession?.outputURL = audioOutputURL
            
            audioExportSession?.exportAsynchronously {
                let error = audioExportSession?.error
                DispatchQueue.main.async {
                    completion(audioOutputURL, error)
                }
            }
    }
}



//
//  FWMQTTManager.swift
//  FuweiCloud
//
//  Created by michael on 2024/5/24.
//

import Foundation
import CocoaMQTT

enum FWMQTTMessageMethodType: String {
case unknown = "unknown"
case otherLogin = "otherLogin"
case otaDownload = "otaDownload"
case otaUpgrade = "otaUpgrade"
case updateDeviceStatus = "updateDeviceStatus" // 设备更新
case addDevice = "addDevice" // 设备新增
case delDevice = "delDevice" // 设备删除
case event = "event"
case playback = "playback"
case deviceLink = "deviceLink" // 设备绑定
case nfEvent = "nfEvent" // 指纹，门卡，掌纹录入进度
}

@objc
enum FWMQMessageNfEventResultType:Int { // 用于nfEvent， 0:录入中 1:录入超时 2:重复录入 3:录入完成 4:录入失败
    case inputing, inputTimeout, inputDuplicate, inputCompleted, inputFailed
}

@objcMembers
class FWMQTTMessage: NSObject {
    var method: FWMQTTMessageMethodType = .unknown
    var methodObjc: String {
        method.rawValue
    }
    var vid: String = ""
    var payload: [String: Any] = [:] // 返回的所有数据
}

class FWMQTTManager: NSObject {
    
    static let shared = FWMQTTManager()
    var isConnected: Bool {
        mqtt5?.connState == .connected
    }
    var connState: CocoaMQTTConnState {
        mqtt5?.connState ?? .disconnected
    }
    private var isConnecting: Bool = false
    @objc static let messageNotification = Notification.Name("MQTTMessageNotification")
    @objc static let connectNotification = Notification.Name("MQTTConnectNotification")
    private var mqtt5: CocoaMQTT5? = nil
    @objc var mqttVidArray = [String]() // 设备订阅的vid数组
    
    @objc func startConnect() {
        guard APGlobal.userInfo != nil && APTools.netReachable() && connState == .disconnected && !isConnecting else { return }
        isConnecting = true
        _ = APRequest(api: .getMQTTParam, parameters: [:], completion: { [unowned self] results, message, code in
            self.isConnecting = false
            if let json = JSON(results ?? [:])["data"].dictionaryObject, nil == message {
                self.connect(json)
            }else if APTools.netReachable() {
                self.perform(APSEL("startConnect"), afterDelay: 6)
            }
        })
    }
    
    @objc static func disconnect() {
        shared.disconnect()
    }
    
    func unsubscribe(_ topic: String) { // 取消订阅
        mqtt5?.unsubscribe(topic)
    }
    
    func subscribe(_ topic: String) { // 订阅
        mqtt5?.subscribe(topic)
    }
    
    func connect(_ param: [String: Any]) {
        if let client_id = param["client_id"] as? String,
           let username = param["username"] as? String,
           let password = param["password"] as? String,
           let keep_alive = param["keep_alive"] as? UInt16,
           let clean_start = param["clean_start"] as? Int,
           let domain = param["domain"] as? String,
           let port = param["port"] as? UInt16,
           let auto_reconnect = param["auto_reconnect"] as? Int,
           let topics = param["topics"] as? [String] {
            print("mqtt param:\(param)")
            let host = domain.nsString.replacingOccurrences(of: "mqtt://", with: "")
            self.mqtt5 = CocoaMQTT5(clientID: client_id, host: host, port: port)
            guard let mqtt5 = self.mqtt5 else { return }
            let connectProperties = MqttConnectProperties()
            connectProperties.topicAliasMaximum = 100
            mqtt5.connectProperties = connectProperties
            mqtt5.username = username
            mqtt5.password = password
            mqtt5.keepAlive = keep_alive
            mqtt5.autoReconnect = auto_reconnect == 1
            mqtt5.cleanSession = clean_start == 1
            DDLogInfo("mqtt prepare connect")
            mqttVidArray = topics.filter({$0.contains("device/")}).map({$0.components(separatedBy: "device/")[1]})
            mqtt5.didConnectAck = { mqtt, reasonCode, ack in
                NotiCenter.post(name: Self.connectNotification, object: true)
                if reasonCode == 0 {
                    DDLogInfo("mqtt connect success")
                    mqtt.subscribe(topics.map{ MqttSubscription(topic: $0) })
                }else {
                    DDLogError("mqtt connect failed:\(reasonCode)")
                }
            }
            mqtt5.didSubscribeTopics = { mqtt, msg, topics, ack in
                DDLogInfo("mqtt didSubscribeTopics: \(topics) message: \(msg)")
            }
            mqtt5.didPublishMessage = { mqtt, msg, code in
                DDLogInfo("mqtt topic: \(msg.topic) didPublishMessage: \(msg)")
            }
            mqtt5.didReceiveMessage = { mqtt, message, port, publish in
                if let msg = message.string {
                    DDLogInfo("mqtt topic: \(message.topic) didReceiveMessage: \(msg)")
                    let json = JSON(parseJSON: msg)
                    let msg = FWMQTTMessage()
                    msg.method = FWMQTTMessageMethodType(rawValue: json["method"].stringValue) ?? .unknown
                    msg.vid = message.topic.contains("/down/device/") ?message.topic.nsString.lastPathComponent:""
                    msg.payload = json.dictionaryObject ?? [:]
                    NotificationCenter.default.post(name: Self.messageNotification, object: msg)
                }
            }
            mqtt5.didDisconnect = { mqtt, error in
                DDLogError("mqtt disconnect error:\(error)")
                guard APGlobal.userInfo != nil else { return }
                NotiCenter.post(name: Self.connectNotification, object: false)
                self.perform(APSEL("startConnect"), afterDelay: 6)
            }
            if !mqtt5.connect(timeout: 30) {
                DDLogError("mqtt connect failed")
            }
        }
    }
    
    func resubscribe() {
        guard APGlobal.userInfo != nil && isConnected else { return }
        _ = APRequest(api: .getMQTTParam, parameters: [:], completion: { [unowned self] results, message, code in
            if let results, let topics = JSON(results)["data"]["topics"].arrayObject as? [String] ,let mqtt5 {
                let topicSet = Set(topics)
                let topicSet1 = Set(mqtt5.subscriptions.map(\.key))
                /// 订阅新增的topic
                mqtt5.subscribe(topicSet.subtracting(topicSet1).map({ MqttSubscription(topic: $0) }))
                /// 取消订阅删除的topic
                mqtt5.unsubscribe(topicSet1.subtracting(topicSet).map({ MqttSubscription(topic: $0) }))
            }
        })
    }
    
    func disconnect() {
        DDLogInfo("mqtt disconnect by user")
        mqtt5?.disconnect()
    }
}


//
//  APMP4Mixer.h
//  arpha plus
//
//  Created by michael on 2023/4/4.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface APMP4Mixer : NSObject

+ (void)rotateMP4:(NSString *)mp4 rotation:(int)rotation completion:(void(^)(BOOL success))completion;

@end

NS_ASSUME_NONNULL_END
//
//  APMP4Mixer.m
//  arpha plus
//
//  Created by michael on 2023/4/4.
//

#import "APMP4Mixer.h"
#import <AVFoundation/AVFoundation.h>

@implementation APMP4Mixer

+ (void)rotateMP4:(NSString *)mp4 rotation:(int)rotation completion:(void (^)(BOOL success))completion {
    NSURL *inputURL = [NSURL fileURLWithPath:mp4];
    AVAsset *asset = [AVAsset assetWithURL:inputURL];
    if (!asset.isPlayable || rotation == 0) {
        completion(NO);
        return;
    }
    AVAssetExportSession *session = [[AVAssetExportSession alloc] initWithAsset:asset presetName:AVAssetExportPresetHighestQuality];
    ///输出文件路径
    NSURL *outputURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:mp4.lastPathComponent]];
    session.outputURL = outputURL;
    session.outputFileType = AVFileTypeMPEG4;
    session.shouldOptimizeForNetworkUse = YES;
    session.videoComposition = [self videoCompositionWithAsset:asset rotation:rotation];
    [session exportAsynchronouslyWithCompletionHandler:^{
        AVAssetExportSessionStatus status = [session status];
        NSLog(@"export video status = %d",(int)status);
        switch (status) {
            case AVAssetExportSessionStatusExporting:
            case AVAssetExportSessionStatusWaiting:
                break;
            case AVAssetExportSessionStatusCompleted:
            {
                NSError *error = nil;
                [[NSFileManager defaultManager] replaceItemAtURL:inputURL
                                                   withItemAtURL:outputURL
                                                  backupItemName:nil
                                                         options:0
                                                resultingItemURL:nil
                                                           error:&error];
                completion(error == nil);
                if (error) {
                    [[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil];
                }
            }
                break;
            default:
            {
                if (status == 4) {
                    NSLog(@"export video error = %@",[session.error localizedFailureReason]);
                }
                [[NSFileManager defaultManager] removeItemAtURL:outputURL error:nil];
                completion(NO);
            }
                break;
        }
    }];
}



+ (AVMutableVideoComposition *)videoCompositionWithAsset:(AVAsset *)asset rotation:(int)rotation {
    NSArray<AVAssetTrack *> * videoTracks = [asset tracksWithMediaType:AVMediaTypeVideo];
    if (videoTracks.count == 0) {
        return nil;
    }
    
    AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
    AVMutableComposition *composition = [AVMutableComposition composition];
    AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
    CGSize videoSize = videoTrack.naturalSize;
    
    // 先设置将要旋转的角度
    CGAffineTransform t1,t2;
    switch (rotation) {
        case 90:// 右转
            t1 = CGAffineTransformMakeTranslation(videoSize.height, 0);// 1,0
            videoSize = CGSizeMake(videoSize.height, videoSize.width);
            break;
        case 180:
            t1 = CGAffineTransformMakeTranslation(videoSize.width, videoSize.height);// 1,1
            break;
        case 270:
            t1 = CGAffineTransformMakeTranslation(0, videoSize.width);// 0,1
            videoSize = CGSizeMake(videoSize.height, videoSize.width);
            break;
        default:
            t1 = CGAffineTransformIdentity;
            break;
    }
    t2 = CGAffineTransformRotate(t1, rotation * M_PI/180);
    // 对调宽高
    composition.naturalSize = videoSize;
    videoComposition.renderSize = videoSize;
    
    videoComposition.frameDuration = CMTimeMakeWithSeconds( 1 / videoTrack.nominalFrameRate, 600);
    
    AVMutableCompositionTrack *compositionVideoTrack;
    compositionVideoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
    [compositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:videoTrack atTime:kCMTimeZero error:nil];
    AVMutableVideoCompositionLayerInstruction *layerInst;
    layerInst = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];
    [layerInst setTransform:t2 atTime:kCMTimeZero];
    
    AVMutableVideoCompositionInstruction *inst = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
    inst.timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration);
    inst.layerInstructions = [NSArray arrayWithObject:layerInst];
    videoComposition.instructions = [NSArray arrayWithObject:inst];
    return videoComposition;
}

@end

//
//  APMP4Writer.h
//  arpha plus
//
//  Created by michael on 2023/4/6.
//

#import <Foundation/Foundation.h>
#import <CoreMedia/CMSampleBuffer.h>

NS_ASSUME_NONNULL_BEGIN

@interface APMP4Writer : NSObject

@property (nonatomic, assign, readonly) BOOL isWritting;
@property (nonatomic, copy, readonly, nullable) NSString *filePath;

- (void)startWithVideoSize:(CGSize)videoSize filePath:(NSString *)path;

- (void)writeVideoBuffer:(CMSampleBufferRef)buffer;

- (void)writePixelBuffer:(CVPixelBufferRef)buffer pts:(CMTime)pts;

- (void)writeAudioBuffer:(CMSampleBufferRef)buffer;

- (void)stopWrite:(void(^)(BOOL success))completion;

@end

NS_ASSUME_NONNULL_END

//
//  APMP4Writer.m
//  arpha plus
//
//  Created by michael on 2023/4/6.
//

#import "APMP4Writer.h"
#import <AVFoundation/AVFoundation.h>

@interface APMP4Writer()

@property (nonatomic, strong) AVAssetWriter *writer;
@property (nonatomic, strong) AVAssetWriterInput *videoInput;
@property (nonatomic, strong) AVAssetWriterInput *audioInput;
@property (nonatomic, strong) AVAssetWriterInputPixelBufferAdaptor *videoInputAdaptor;
@property (nonatomic,   copy) NSString *filePath;
@property (nonatomic, assign) BOOL isWritting;

@end

@implementation APMP4Writer


- (void)startWithVideoSize:(CGSize)videoSize filePath:(NSString *)path {
    self.filePath = nil;
    self.isWritting = YES;
    NSError *error = nil;
    self.writer = [AVAssetWriter assetWriterWithURL:[NSURL fileURLWithPath:path] fileType:AVFileTypeQuickTimeMovie error:&error];
    if (error) NSLog(@"writerTag: asset writer create failed:%@",error);
    
    //H264(AVC)压缩编码参数
    NSDictionary *videoSetting = @{AVVideoCodecKey:AVVideoCodecTypeH264,//编码格式
                                   AVVideoWidthKey:@(videoSize.width),
                                   AVVideoHeightKey:@(videoSize.height)
    };
    //AAC压缩编码参数
    NSDictionary *audioSetting = @{AVFormatIDKey:@(kAudioFormatMPEG4AAC),
                                   AVSampleRateKey:@8000,
                                   AVNumberOfChannelsKey:@1,
                                   AVEncoderBitRateKey:@32000
    };
    self.videoInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeVideo outputSettings:videoSetting];
    self.videoInput.expectsMediaDataInRealTime = YES;
    self.videoInput.naturalSize = videoSize;
//    self.videoInput.transform = CGAffineTransformMakeRotation(90);
    self.videoInputAdaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:self.videoInput sourcePixelBufferAttributes:nil];
    self.audioInput = [AVAssetWriterInput assetWriterInputWithMediaType:AVMediaTypeAudio outputSettings:audioSetting];
    self.audioInput.expectsMediaDataInRealTime = YES;
    if ([self.writer canAddInput:self.videoInput]) {
        [self.writer addInput:self.videoInput];
    }
//    if ([self.writer canAddInput:self.audioInput]) {
//        [self.writer addInput:self.audioInput];
//    }
    [self.writer startWriting];
    [self.writer startSessionAtSourceTime:kCMTimeZero];
}

- (void)writeVideoBuffer:(CMSampleBufferRef)buffer {
    if (self.isWritting && self.videoInput.isReadyForMoreMediaData) {
        [self.videoInput appendSampleBuffer:buffer];
    }
}

- (void)writePixelBuffer:(CVPixelBufferRef)buffer pts:(CMTime)pts {
    if (self.isWritting && self.videoInput.isReadyForMoreMediaData) {
        [self.videoInputAdaptor appendPixelBuffer:buffer withPresentationTime:pts];
    }
}

- (void)writeAudioBuffer:(CMSampleBufferRef)buffer {
//    if (self.isWritting && self.audioInput.isReadyForMoreMediaData) {
//        [self.audioInput appendSampleBuffer:buffer];
//    }
}






- (void)stopWrite:(void (^)(BOOL))completion {
    [self.videoInput markAsFinished];
//    [self.audioInput markAsFinished];
    self.isWritting = NO;
    MJWeakSelf
    [self.writer finishWritingWithCompletionHandler:^{
        switch (weakSelf.writer.status) {
            case AVAssetWriterStatusCompleted:
                NSLog(@"writerTag: asset write completed");
                break;
            case AVAssetWriterStatusFailed:
                NSLog(@"writerTag: asset write failed:%@",weakSelf.writer.error);
                break;
            default:
                break;
        }
        completion(weakSelf.writer.status == AVAssetWriterStatusCompleted);
    }];
}

@end

//
//  YYTextView+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/11/16.
//

#import "YYTextView+APExtension.h"

@implementation YYTextView (APExtension)

+ (instancetype)textViewWithText:(NSString *)text font:(id)font color:(UIColor *)color {
    return [self textViewWithAString:AString(text, font, color)];
}

+ (instancetype)textViewWithFont:(id)font color:(UIColor *)color placeholder:(NSString *)placeholder {
    let tv = [self textViewWithText:nil font:font color:color];
    let placeholderAttr = AString(placeholder, tv.font, [color colorWithAlphaComponent:0.5]);
    tv.placeholderAttributedText = placeholderAttr;
    return tv;
}

+ (instancetype)textViewWithAString:(NSAttributedString *)attributedString {
    let tv = [[self alloc] init];
    tv.textContainerInset = UIEdgeInsetsZero;
    tv.contentInset = UIEdgeInsetsZero;
    tv.attributedText = attributedString;
    return tv;
}

+ (instancetype)textViewWithString:(id)string font:(id)font color:(UIColor *)color {
    if (nil == string || [string isKindOfClass:[NSString class]]) {
        return [self textViewWithText:string font:font color:color];
    }
    return [self textViewWithAString:string];
}



- (void)setString:(id)string {
    if (nil == string || [string isKindOfClass:[NSString class]]) {
        self.text = string;
    }else {
        self.attributedText = string;
    }
}

- (id)string {
    if (self.attributedText) return self.attributedText;
    return self.text;
}

- (CGFloat)textHeightForWidth:(CGFloat)width {
    if ([self.attributedText length]) {
        return [self.attributedText heightForWidth:width];
    }
    return [self.text heightForFont:self.font width:width];
}

- (CGFloat)textWidth {
    if ([self.attributedText length]) {
        return [self.attributedText width];
    }
    return [self.text widthForFont:self.font];
}
@end

//
//  YYTextView+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/11/16.
//

#import "YYTextView.h"

NS_ASSUME_NONNULL_BEGIN

@interface YYTextView (APExtension)

+ (instancetype)textViewWithText:(nullable NSString *)text font:(id)font color:(UIColor *)color;
+ (instancetype)textViewWithFont:(id)font color:(UIColor *)color placeholder:(nullable NSString *)placeholder;
+ (instancetype)textViewWithAString:(NSAttributedString *)attributedString;

- (CGFloat)textHeightForWidth:(CGFloat)width;

- (CGFloat)textWidth;

+ (instancetype)textViewWithString:(id)string font:(id)font color:(UIColor *)color;

@property (nonatomic, copy, nullable) id string;

@end

NS_ASSUME_NONNULL_END

//
//  YYLabel+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/9/14.
//

#import "YYLabel+APExtension.h"

@implementation YYLabel (APExtension)

- (CGFloat)textHeightForWidth:(CGFloat)width {
    if ([self.attributedText length]) {
        return [self.attributedText heightForWidth:width];
    }
    return [self.text heightForFont:self.font width:width];
}

- (CGFloat)textWidth {
    if ([self.attributedText length]) {
        return [self.attributedText width];
    }
    return [self.text widthForFont:self.font];
}

+ (instancetype)labelWithText:(NSString *)text font:(id)font color:(UIColor *)color {
    YYLabel *l = [[YYLabel alloc] init];
    l.text = text;
    l.font = [UIFont IDFont:font];
    l.textColor = color;
    return l;
}


+ (instancetype)labelWithFont:(id)font color:(UIColor *)color {
    return [self labelWithText:nil font:font color:color];
}



+ (instancetype)labelWithAString:(NSAttributedString *)attributedString {
    YYLabel *l = [[YYLabel alloc] init];
    l.attributedText = attributedString;
    return l;
}

+ (instancetype)labelWithString:(id)string font:(id)font color:(UIColor *)color {
    if (nil == string || [string isKindOfClass:[NSString class]]) {
        return [self labelWithText:string font:font color:color];
    }
    return [self labelWithAString:string];
}



- (void)setString:(id)string {
    if (nil == string || [string isKindOfClass:[NSString class]]) {
        self.text = string;
    }else {
        self.attributedText = string;
    }
}

- (id)string {
    if (self.attributedText) return self.attributedText;
    return self.text;
}


@end

//
//  YYLabel+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/9/14.
//

#import "YYLabel.h"

NS_ASSUME_NONNULL_BEGIN

@interface YYLabel (APExtension)

+ (instancetype)labelWithText:(nullable NSString *)text font:(id)font color:(UIColor *)color;
+ (instancetype)labelWithFont:(id)font color:(UIColor *)color;
+ (instancetype)labelWithAString:(NSAttributedString *)attributedString;

+ (instancetype)labelWithString:(id)string font:(id)font color:(UIColor *)color;


@property (nonatomic, copy, nullable) id string;

- (CGFloat)textHeightForWidth:(CGFloat)width;

- (CGFloat)textWidth;


@end

NS_ASSUME_NONNULL_END

//
//  UIViewController+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/5/3.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, FWControllerStactPosition) {
    FWControllerStactPositionUninit,
    FWControllerStactPositionOnTop,
    FWControllerStactPositionPushed,
};

@interface UIViewController (APExtension)

- (BOOL)netConfigPopBack:(Class)cls;

+ (UIViewController *)currentController;

+ (UIViewController *)currentTopViewController;

/// self为present栈，栈顶
- (__kindof UIViewController *)dismissToRootViewControllerAnimated:(BOOL)flag completion:(void (^ __nullable)(void))completion;

/// self为present栈，栈底
- (void)dismissAllPresentedControllersAnimated:(BOOL)flag completion:(void (^ __nullable)(void))completion;

- (void)pushVC:(UIViewController *)vc;
- (void)pushVC:(UIViewController *)vc animated:(BOOL)flag;

- (void)popVC;
- (void)popVC:(BOOL)flag;

- (void)popToVCWhich:(BOOL(^)(UIViewController *vc))comletion animated:(BOOL)flag;
- (void)popToVCWhich:(BOOL(^)(UIViewController *vc))comletion;
- (void)popToVCWhich:(BOOL(^)(UIViewController *vc))comletion elseToRootVC:(BOOL)toRootVC; // toRootVC

- (void)popToVC:(UIViewController *)vc;
- (void)popToVC:(UIViewController *)vc animated:(BOOL)flag;

- (void)popToRootVC;
- (void)popToRootVC:(BOOL)flag;


- (void)presentVC:(UIViewController *)vc;
- (void)presentVC:(UIViewController *)vc animated:(BOOL)flag;

- (void)dismissVC;
- (void)dismissVC:(BOOL)flag;
- (void)dismissVC:(BOOL)flag completion:(dispatch_block_t)completion;

- (void)setViewControllersWhich:(BOOL(^)(UIViewController *vc))comletion;

- (void)pushPositionOf:(Class)vcClass completion:(void(^ __nullable)(FWControllerStactPosition position, UIViewController *_Nullable vc))completion;

@end

NS_ASSUME_NONNULL_END

//
//  UIViewController+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/5/3.
//

#import "UIViewController+APExtension.h"
#import "APBaseCategoryController.h"
#ifdef ARPHA_PLUS
#import "ArphaPlus-Swift.h"
#elif CLDBUS
#import "Cldbus-Swift.h"
#else
#import "LNDU-Swift.h"
#endif

@implementation UIViewController (APExtension)

+ (void)load {
    [self swizzleInstanceMethod:@selector(presentViewController:animated:completion:) with:@selector(ap_presentViewController:animated:completion:)];
}

//+ (void)initialize {
//    static dispatch_once_t onceToken;
//    dispatch_once(&onceToken, ^{
//
//    });
//}

- (void)ap_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {
    if (viewControllerToPresent.modalPresentationStyle != UIModalPresentationFullScreen) {
        viewControllerToPresent.modalPresentationStyle = UIModalPresentationFullScreen;
    }
    [self ap_presentViewController:viewControllerToPresent animated:flag completion:completion];
}

- (BOOL)netConfigPopBack:(Class)cls {
    return [self isKindOfClass:[FWMineViewController class]] || [self isKindOfClass:[FWDeviceListController class]] || [self isKindOfClass:cls];
}

- (void)setViewControllersWhich:(BOOL (^)(UIViewController * _Nonnull vc))comletion {
    let selectVCs = [self.navigationController.viewControllers select:comletion];
    [self.navigationController setViewControllers:selectVCs];
}

+ (UIViewController *)currentController {
    return [self currentViewControllerWithRoot:APKeyWindow.rootViewController];
}


+ (UIViewController *)currentTopViewController {
    var vc = [self currentController];
    return [vc dismissToRootViewControllerAnimated:NO completion:nil];
}

+ (UIViewController *)currentViewControllerWithRoot:(UIViewController*)root {
    if (root.presentedViewController) {
        UIViewController* presentedViewController = root.presentedViewController;
        return [self currentViewControllerWithRoot:presentedViewController];
    } else if ([root isKindOfClass:[UINavigationController class]]) {
        UINavigationController* navigationController = (UINavigationController*)root;
        return [self currentViewControllerWithRoot:navigationController.visibleViewController];
    } else if ([root isKindOfClass:[UITabBarController class]]) {
        UITabBarController *tabBarController = (UITabBarController *)root;
        return [self currentViewControllerWithRoot:tabBarController.selectedViewController];
    } else {
        return root;
    }
}


- (UIViewController *)dismissToRootViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
    __weak UIViewController *vc = self;
    while (vc.presentingViewController) {
        vc = vc.presentingViewController;
    }
    [vc dismissViewControllerAnimated:flag completion:completion];
    return vc;
}

- (void)dismissAllPresentedControllersAnimated:(BOOL)flag completion:(void (^)(void))completion {
    if (self.presentedViewController) {
        [self.presentedViewController dismissViewControllerAnimated:flag completion:completion];
    }
}

- (void)pushVC:(UIViewController *)vc animated:(BOOL)flag {
    [[self navVC] pushViewController:vc animated:flag];
}

- (void)pushVC:(UIViewController *)vc {
    [self pushVC:vc animated:YES];
}

- (UINavigationController *)navVC {
    return (UINavigationController *)([self isKindOfClass:[UINavigationController class]] ? self : self.navigationController);
}

- (void)popVC:(BOOL)flag {
    [[self navVC] popViewControllerAnimated:flag];
}
- (void)popVC {
    [self popVC:YES];
}

- (void)popToVCWhich:(BOOL (^)(UIViewController * _Nonnull))comletion {
    [self popToVCWhich:comletion animated:YES];
}

- (void)popToVCWhich:(BOOL (^)(UIViewController * _Nonnull))comletion animated:(BOOL)flag {
    NSArray<UIViewController *> *vcs = [[self navVC] viewControllers];
    UIViewController *vc = [vcs match:comletion];
    if (vc) {
        [self popToVC:vc animated:flag];
    }
}

- (void)popToVCWhich:(BOOL(^)(UIViewController *vc))comletion elseToRootVC:(BOOL)toRootVC{
    NSArray<UIViewController *> *vcs = [[self navVC] viewControllers];
    UIViewController *vc = [vcs match:comletion];
    if (vc) {
        [self popToVC:vc animated:YES];
    }else if (toRootVC) {
        [self popToRootVC];
    }
}


- (void)popToVC:(UIViewController *)vc animated:(BOOL)flag {
    [[self navVC] popToViewController:vc animated:flag];
}
- (void)popToVC:(UIViewController *)vc {
    [self popToVC:vc animated:YES];
}

- (void)popToRootVC:(BOOL)flag {
    [[self navVC] popToRootViewControllerAnimated:flag];
}
- (void)popToRootVC {
    [self popToRootVC:YES];
}

- (void)presentVC:(UIViewController *)vc animated:(BOOL)flag {
    [self presentViewController:vc animated:flag completion:nil];
}

- (void)presentVC:(UIViewController *)vc {
    [self presentViewController:vc animated:YES completion:nil];
}

- (void)dismissVC:(BOOL)flag completion:(dispatch_block_t)completion {
    [self dismissViewControllerAnimated:flag completion:completion];
}

- (void)dismissVC:(BOOL)flag {
    [self dismissViewControllerAnimated:flag completion:nil];
}

- (void)dismissVC {
    [self dismissViewControllerAnimated:YES completion:nil];
}

- (void)pushPositionOf:(Class)vcClass completion:(void (^)(FWControllerStactPosition, UIViewController * _Nullable))completion {
    UINavigationController *nvc = nil;
    if ([self isKindOfClass:[UINavigationController class]]) {
        nvc = (UINavigationController *)self;
    }else {
        nvc = self.navigationController;
    }
    UIViewController *pvc = [nvc.viewControllers match:^BOOL(__kindof UIViewController * _Nonnull obj) {
        return [[vcClass className] isEqualToString:obj.className];
    }];
    if (pvc) {
        if (pvc == nvc.topViewController) {
            completion(FWControllerStactPositionOnTop, pvc);
        }else {
            completion(FWControllerStactPositionPushed, pvc);
        }
    }else {
        completion(FWControllerStactPositionUninit, nil);
    }
}

@end

//
//  UIView+APExtension.swift
//  FuweiCloud
//
//  Created by michael on 2024/6/11.
//

import UIKit

private var blurViewKey: UInt8 = 0
extension UIView {
    func addLine(_ type: LSSeparateLineType = .top, cap: CGFloat = 0, padding: CGSize = .zero, lineWidth: CGFloat = 1/YYScreenScale(), color: UIColor = .separator) {
        let line = UIView(color: color)
        addSubview(line)
        line.snp.makeConstraints { make in
            switch type {
            case .top, .bottom:
                if type == .top {
                    make.top.equalTo(cap)
                }else {
                    make.bottom.equalTo(-cap)
                }
                make.left.equalTo(padding.width)
                make.right.equalTo(-padding.height)
                make.height.equalTo(lineWidth)
            case .left, .right:
                if type == .left {
                    make.left.equalTo(cap)
                }else {
                    make.right.equalTo(-cap)
                }
                make.top.equalTo(padding.width)
                make.bottom.equalTo(-padding.height)
                make.width.equalTo(lineWidth)
            @unknown default:
                break
            }
        }
    }
    
    var blurView: VisualEffectView? {
        set {
            setAssociateValue(newValue, withKey: &blurViewKey)
            if let newValue {
                newValue.contentView.addSubview(self)
                snp.makeConstraints { make in
                    make.edges.equalToSuperview()
                }
            }
        }
        get {
            associatedValue(forKey: &blurViewKey) as? VisualEffectView
        }
    }
}

//
//  UIView+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/3/24.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSInteger, LSSeparateLineType) {
    LSSeparateLineTop,
    LSSeparateLineBottom,
    LSSeparateLineLeft,
    LSSeparateLineRight
};

#define APViewColored(_c_) [UIView viewWithColor:_c_]

@interface APCornersView : UIView

- (void)setCorners:(UIRectCorner)corners radii:(CGSize)radii;

- (void)setBorderWidth:(CGFloat)width color:(nullable UIColor *)color;

- (void)setShadowColor:(UIColor *)sColor shadowRadius:(CGFloat)sRadius shadowOffset:(CGSize)offset;

@end

@interface UIView (APExtension)

/// 用于辅助定位子视图
@property (nonatomic, assign, readonly) NSUInteger viewTag;

+ (instancetype)viewWithColor:(UIColor *)color;
+ (instancetype)viewWithColor:(UIColor *)color cornerRadiu:(CGFloat)cornerRadiu;

- (void)addSubviews:(NSArray<UIView *> *)subviews;

- (NSArray<UIView *> *)allSubviews;

- (UIView *)addLine:(LSSeparateLineType)type width:(CGFloat)lineWidth color:(UIColor *)color offset:(CGPoint)offset;

- (UIView *)addLine:(LSSeparateLineType)type width:(CGFloat)lineWidth color:(UIColor *)color;

- (UIView *)addLine:(LSSeparateLineType)type color:(UIColor *)color;

- (UIView *)addLine:(LSSeparateLineType)type;

/// 添加圆角，边框
- (void)addCorner:(CGFloat)radius;
- (void)addCorner:(CGFloat)radius borderWidth:(CGFloat)bWidth borderColor:(nullable UIColor *)bColor;

/// 添加异形圆角，边框
- (void)addCorners:(UIRectCorner)corners radii:(CGSize)radii;
- (void)addCorners:(UIRectCorner)corners radii:(CGSize)radii borderWidth:(CGFloat)bWidth borderColor:(nullable UIColor *)bColor;

/// 添加阴影
- (void)addShadowColor:(UIColor *)color radius:(CGFloat)radius offset:(CGSize)offset;

///添加圆角和阴影，边框
- (void)addCornerAndShadow:(CGFloat)cRadius shadowColor:(nullable UIColor *)sColor shadowRadius:(CGFloat)sRadius shadowOffset:(CGSize)offset;
- (void)addCornerAndShadow:(CGFloat)cRadius borderWidth:(CGFloat)bWidth borderColor:(nullable UIColor *)bColor shadowColor:(nullable UIColor *)sColor shadowRadius:(CGFloat)sRadius shadowOffset:(CGSize)offset;

///添加异形圆角和阴影，边框
- (void)addCornersAndShadow:(UIRectCorner)corners radii:(CGSize)radii shadowColor:(nullable UIColor *)sColor shadowRadius:(CGFloat)sRadius shadowOffset:(CGSize)offset;
- (void)addCornersAndShadow:(UIRectCorner)corners radii:(CGSize)radii borderWidth:(CGFloat)bWidth borderColor:(nullable UIColor *)bColor shadowColor:(nullable UIColor *)sColor shadowRadius:(CGFloat)sRadius shadowOffset:(CGSize)offset;


- (void)addTapWithTouches:(NSUInteger)numberOfTouches taps:(NSUInteger)numberOfTaps handler:(void(^)(UIGestureRecognizer *sender, UIGestureRecognizerState state, CGPoint location))handler;

- (void)addTapAction:(SEL)action target:(nullable id)target;
- (void)addDoubleTapAction:(SEL)action target:(nullable id)target;
- (void)addTap:(void(^)(UIGestureRecognizer *sender, UIGestureRecognizerState state, CGPoint location))handler;
- (void)addDoubleTap:(void(^)(UIGestureRecognizer *sender, UIGestureRecognizerState state, CGPoint location))handler;

- (void)removeAllGestures;
@end

NS_ASSUME_NONNULL_END

//
//  UIView+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/3/24.
//

#import "UIView+APExtension.h"

@implementation APCornersView {
    UIRectCorner _corners;
    CGSize _radii;
    CGFloat _borderWidth;
    UIColor *_borderColor;
    UIColor *_fillColor;
    CGSize _shadowOffset;
    CGFloat _shadowRadius;
    UIColor *_shadowColor;
}

- (instancetype)initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {
        self.userInteractionEnabled = NO;
    }
    return self;
}


- (void)setCorners:(UIRectCorner)corners radii:(CGSize)radii {
    _corners = corners;
    _radii = radii;
    [self setNeedsDisplay];
}

- (void)setShadowColor:(UIColor *)sColor shadowRadius:(CGFloat)sRadius shadowOffset:(CGSize)offset {
    _shadowColor = sColor;
    _shadowRadius = sRadius;
    _shadowOffset = offset;
}

- (void)setBorderWidth:(CGFloat)width color:(UIColor *)color {
    _borderWidth = width;
    _borderColor = color;
    [self setNeedsDisplay];
}

- (void)setBackgroundColor:(UIColor *)backgroundColor {
    [super setBackgroundColor:[UIColor clearColor]];
    _fillColor = backgroundColor;
}

- (void)drawRect:(CGRect)rect {
    UIBezierPath *fillPath = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:_corners cornerRadii:_radii];
    if (_shadowColor && _shadowRadius) {
        if (self.superview) {
            [self.superview addShadowColor:_shadowColor radius:_shadowRadius offset:_shadowOffset];
            self.superview.layer.shadowPath = fillPath.CGPath;
        }
    }
    [_fillColor setFill];
    [fillPath fill];
    if (_borderWidth && _borderColor) {
        CGFloat cap = _borderWidth / 2;
        UIBezierPath *strokePath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, cap, cap) byRoundingCorners:_corners cornerRadii:CGSizeMake(_radii.width - cap, _radii.height - cap)];
        strokePath.lineWidth = _borderWidth;
        [_borderColor setStroke];
        [strokePath stroke];
    }
}

@end



@implementation UIView (APExtension)

- (NSUInteger)viewTag {
    return (NSUInteger)self;
}

- (NSArray<UIView *> *)allSubviews {
    __block NSArray* allSubviews = [NSArray arrayWithObject:self];
    [self.subviews enumerateObjectsUsingBlock:^(UIView* view, NSUInteger idx, BOOL*stop) {
        allSubviews = [allSubviews arrayByAddingObjectsFromArray:[view allSubviews]];
    }];
    return allSubviews;
}

+ (instancetype)viewWithColor:(UIColor *)color {
    UIView *view = [[self alloc] init];
    view.backgroundColor = color;
    return view;
}

+ (instancetype)viewWithColor:(UIColor *)color cornerRadiu:(CGFloat)cornerRadiu {
    UIView *view = [[self alloc] init];
    view.backgroundColor = color;
    if (![view isKindOfClass:[UIView class]]) {
        view.clipsToBounds = YES;
    }
    view.layer.cornerRadius = cornerRadiu;
    return view;
}

- (void)addSubviews:(NSArray<UIView *> *)subviews {
    [subviews enumerateObjectsUsingBlock:^(UIView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [self addSubview:obj];
    }];
}

- (UIView *)addLine:(LSSeparateLineType)type width:(CGFloat)lineWidth color:(UIColor *)color offset:(CGPoint)offset {
    UIView *line = [UIView new];
    line.backgroundColor = color;
    [self addSubview:line];
    [line mas_makeConstraints:^(MASConstraintMaker *make) {
        switch (type) {
            case LSSeparateLineTop:{
                make.left.mas_equalTo(offset.x);
                make.right.mas_equalTo(-offset.y);
                make.height.mas_equalTo(lineWidth);
                make.top.mas_equalTo(0);
            }
                break;
            case LSSeparateLineRight:{
                make.top.mas_equalTo(offset.x);
                make.bottom.mas_equalTo(-offset.y);
                make.width.mas_equalTo(lineWidth);
                make.right.mas_equalTo(0);
            }
                break;
            case LSSeparateLineBottom:{
                make.left.mas_equalTo(offset.x);
                make.right.mas_equalTo(-offset.y);
                make.height.mas_equalTo(lineWidth);
                make.bottom.mas_equalTo(0);
            }
                break;
            default:{
                make.top.mas_equalTo(offset.x);
                make.bottom.mas_equalTo(-offset.y);
                make.width.mas_equalTo(lineWidth);
                make.left.mas_equalTo(0);
            }
                break;
        }
    }];
    return line;
}

- (UIView *)addLine:(LSSeparateLineType)type width:(CGFloat)lineWidth color:(UIColor *)color {
    return [self addLine:type width:lineWidth color:color offset:CGPointZero];
}

- (UIView *)addLine:(LSSeparateLineType)type color:(UIColor *)color {
    return [self addLine:type width:1/[UIScreen mainScreen].scale color:color offset:CGPointZero];
}

- (UIView *)addLine:(LSSeparateLineType)type {
    return [self addLine:type width:1/[UIScreen mainScreen].scale color:APSeperator offset:CGPointZero];
}



- (void)addCorner:(CGFloat)radius {
    [self addCorner:radius borderWidth:0 borderColor:nil];
}
- (void)addCorner:(CGFloat)radius borderWidth:(CGFloat)bWidth borderColor:(UIColor *)bColor {
    ///如果是UIView的子类则必须设置clipsToBounds
    if (![self isMemberOfClass:[UIView class]]) {
        self.clipsToBounds = YES;
    }
    self.layer.cornerRadius = radius;
    if (bColor) {
        self.layer.borderColor = bColor.CGColor;
        self.layer.borderWidth = bWidth;
    }
}

- (void)addCorners:(UIRectCorner)corners radii:(CGSize)radii {
    [self addCornersAndShadow:corners radii:radii borderWidth:0 borderColor:nil shadowColor:nil shadowRadius:0 shadowOffset:CGSizeZero];
}

- (void)addCorners:(UIRectCorner)corners radii:(CGSize)radii borderWidth:(CGFloat)bWidth borderColor:(UIColor *)bColor {
    [self addCornersAndShadow:corners radii:radii borderWidth:bWidth borderColor:bColor shadowColor:nil shadowRadius:0 shadowOffset:CGSizeZero];
}

- (void)addShadowColor:(UIColor *)color radius:(CGFloat)radius offset:(CGSize)offset {
    if (radius == 0 || color == nil) return;
    if (![self isMemberOfClass:[UIView class]]) {
        self.clipsToBounds = NO;
    }
    self.layer.shadowColor = color.CGColor;
    self.layer.shadowOffset = offset;
    self.layer.shadowRadius = radius;
    self.layer.shadowOpacity = 1;
}



- (void)addCornerAndShadow:(CGFloat)cRadius shadowColor:(UIColor *)sColor shadowRadius:(CGFloat)sRadius shadowOffset:(CGSize)offset {
    [self addCornersAndShadow:UIRectCornerAllCorners radii:CGSizeMake(cRadius, cRadius) borderWidth:0 borderColor:nil shadowColor:sColor shadowRadius:sRadius shadowOffset:offset];
}

- (void)addCornerAndShadow:(CGFloat)cRadius borderWidth:(CGFloat)bWidth borderColor:(UIColor *)bColor shadowColor:(UIColor *)sColor shadowRadius:(CGFloat)sRadius shadowOffset:(CGSize)offset {
    [self addCornersAndShadow:UIRectCornerAllCorners radii:CGSizeMake(cRadius, cRadius) borderWidth:bWidth borderColor:bColor shadowColor:sColor shadowRadius:sRadius shadowOffset:offset];
}


- (void)addCornersAndShadow:(UIRectCorner)corners radii:(CGSize)radii shadowColor:(UIColor *)sColor shadowRadius:(CGFloat)sRadius shadowOffset:(CGSize)offset {
    [self addCornersAndShadow:corners radii:radii borderWidth:0 borderColor:nil shadowColor:sColor shadowRadius:sRadius shadowOffset:offset];
}

- (void)addCornersAndShadow:(UIRectCorner)corners radii:(CGSize)radii borderWidth:(CGFloat)bWidth borderColor:(UIColor *)bColor shadowColor:(UIColor *)sColor shadowRadius:(CGFloat)sRadius shadowOffset:(CGSize)offset {
    ///之所以用视图做圆角蒙版，是因为视图可以设置约束更方便
    APCornersView *cornerView = [[APCornersView alloc] init];
    [cornerView setShadowColor:sColor shadowRadius:sRadius shadowOffset:offset];
    [cornerView setCorners:corners radii:radii];
    [cornerView setBorderWidth:bWidth color:bColor];
    [self addSubview:cornerView];
    [self sendSubviewToBack:cornerView];
    //让圆角视图颜色和自己一致，然后在自己设置为透明
    cornerView.backgroundColor = [self.backgroundColor copy];
    self.backgroundColor = [UIColor clearColor];
    [cornerView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.edges.mas_equalTo(0);
    }];
//    [self addShadowColor:sColor radius:sRadius offset:offset];
}



- (void)addTapWithTouches:(NSUInteger)numberOfTouches taps:(NSUInteger)numberOfTaps handler:(void(^)(UIGestureRecognizer *, UIGestureRecognizerState, CGPoint))handler
{
    if (!handler) return;
    self.userInteractionEnabled = true;
    UITapGestureRecognizer *gesture = [UITapGestureRecognizer recognizerWithHandler:handler];
    
    gesture.numberOfTouchesRequired = numberOfTouches;
    gesture.numberOfTapsRequired = numberOfTaps;

    [self.gestureRecognizers enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
        if (![obj isKindOfClass:[UITapGestureRecognizer class]]) return;

        UITapGestureRecognizer *tap = obj;
        BOOL rightTouches = (tap.numberOfTouchesRequired == numberOfTouches);
        BOOL rightTaps = (tap.numberOfTapsRequired == numberOfTaps);
        if (rightTouches && rightTaps) {
            [gesture requireGestureRecognizerToFail:tap];
        }
    }];
    [self addGestureRecognizer:gesture];
}

- (void)addTapAction:(SEL)action target:(id)target {
    self.userInteractionEnabled = YES;
    [self addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:target action:action]];
}

- (void)addDoubleTapAction:(SEL)action target:(id)target {
    let tap = [[UITapGestureRecognizer alloc] initWithTarget:target action:action];
    tap.numberOfTapsRequired = 2;
    [self addGestureRecognizer:tap];
}

- (void)addTap:(void(^)(UIGestureRecognizer *, UIGestureRecognizerState, CGPoint))handler {
    [self addTapWithTouches:1 taps:1 handler:handler];
}

- (void)addDoubleTap:(void(^)(UIGestureRecognizer *, UIGestureRecognizerState, CGPoint))handler {
    [self addTapWithTouches:1 taps:2 handler:handler];
}

- (void)removeAllGestures {
    [self.gestureRecognizers enumerateObjectsUsingBlock:^(__kindof UIGestureRecognizer * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [self removeGestureRecognizer:obj];
    }];
}


@end

//
//  UITextView+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/11/18.
//

#import "UITextView+APExtension.h"

@implementation UITextView (APExtension)

- (CGFloat)textHeightForWidth:(CGFloat)width {
    if ([self.attributedText length]) {
        return [self.attributedText heightForWidth:width];
    }
    return [self.text heightForFont:self.font width:width];
}

- (CGFloat)textWidth {
    if ([self.attributedText length]) {
        return [self.attributedText width];
    }
    return [self.text widthForFont:self.font];
}

@end
//
//  UITextView+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/11/18.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UITextView (APExtension)

- (CGFloat)textHeightForWidth:(CGFloat)width;

- (CGFloat)textWidth;

@end

NS_ASSUME_NONNULL_END

//
//  UITextField+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/4/27.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UITextField (APExtension)

@property (nonatomic, assign) CGFloat leftMargin;
@property (nonatomic, assign) CGFloat rightMargin;

+ (instancetype)textFieldWithFont:(id)font color:(UIColor *)color placeholder:(nullable id)placeholder;
+ (instancetype)textFieldWithFont:(id)font placeholder:(nullable id)placeholder;
+ (instancetype)textFieldWithPlaceholder:(nullable id)placeholder;

@end

NS_ASSUME_NONNULL_END
//
//  UITextField+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/4/27.
//

#import "UITextField+APExtension.h"


@implementation UITextField (APExtension)
@dynamic leftMargin, rightMargin;

- (void)setLeftMargin:(CGFloat)leftMargin {
    self.leftView = [[UIImageView alloc] initWithImage:[UIImage imageWithColor:[UIColor clearColor] size:CGSizeMake(leftMargin, 10)]];
    self.leftViewMode = UITextFieldViewModeAlways;
}

- (void)setRightMargin:(CGFloat)rightMargin {
    self.rightView = [[UIImageView alloc] initWithImage:[UIImage imageWithColor:[UIColor clearColor] size:CGSizeMake(rightMargin, 10)]];
    self.rightViewMode = UITextFieldViewModeAlways;
}


+ (instancetype)textFieldWithFont:(id)font color:(UIColor *)color placeholder:(nullable id)placeholder {
    UITextField *tf = [[UITextField alloc] init];
    tf.autocorrectionType = UITextAutocorrectionTypeNo;
    tf.spellCheckingType = UITextSpellCheckingTypeNo;
    tf.smartQuotesType = UITextSmartQuotesTypeNo;
    tf.smartDashesType = UITextSmartDashesTypeNo;
    tf.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo;
    tf.textContentType = UITextContentTypeOneTimeCode;
    tf.backgroundColor = [UIColor clearColor];
    tf.font = [UIFont IDFont:font];
    tf.textColor = color;
    
    if (placeholder) {
        if ([placeholder isKindOfClass:[NSString class]]) {
            tf.placeholder = placeholder;
        }else {
            tf.attributedPlaceholder = placeholder;
        }
    }
    tf.returnKeyType = UIReturnKeyDone;
    [tf setShouldReturnBlock:^BOOL(UITextField * _Nonnull textField) {
        [textField resignFirstResponder];
        return YES;
    }];
    return tf;
}

+ (instancetype)textFieldWithFont:(id)font placeholder:(id)placeholder {
    return [self textFieldWithFont:font color:[UIColor labelColor] placeholder:placeholder];
}

+ (instancetype)textFieldWithPlaceholder:(id)placeholder {
    return [self textFieldWithFont:@16 color:[UIColor labelColor] placeholder:placeholder];
}



@end

//
//  UITableViewCell+LSExtension.h
//  LPDoorBell
//
//  Created by Champion Fu on 2020/5/13.
//  Copyright © 2020 lancens_app. All rights reserved.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UITableViewCell (LSExtension)

@property (nonatomic, weak, readonly, nullable) UITableView *tableView;

@property (nonatomic, assign) CGFloat separatorOffset;

@end

NS_ASSUME_NONNULL_END
//
//  UITableViewCell+NSMutableAttributedString+LSExtension.m
//  LPDoorBell
//
//  Created by Champion Fu on 2020/5/13.
//  Copyright © 2020 lancens_app. All rights reserved.
//

#import "UITableViewCell+LSExtension.h"

@implementation UITableViewCell (LSExtension)

-(UITableView *)tableView {
    UIView *superView = self.superview;
    while (superView && ![superView isKindOfClass:[UITableView class]]) {
        superView = superView.superview;
    }
    return (UITableView *)superView;
}

- (void)setSeparatorOffset:(CGFloat)seperatorOffset {
    self.separatorInset = UIEdgeInsetsMake(0, MAX(0, seperatorOffset), 0, seperatorOffset<0?CGFLOAT_MAX:0);
}

- (CGFloat)separatorOffset {
    return self.separatorInset.left;
}


@end

//
//  UITableView+APExtension.swift
//  FuweiCloud
//
//  Created by michael on 2024/5/29.
//

import UIKit

extension UITableView {
    func dequeueReusableCell<T: UITableViewCell>(named name: T.Type) -> T {
        guard let cell = dequeueReusableCell(withIdentifier: NSStringFromClass(name)) as? T else {
            fatalError(
                "Couldn't find UITableViewCell for \(NSStringFromClass(name)), make sure the cell is registered with table view")
        }
        return cell
    }

    func dequeueReusableCell<T: UITableViewCell>(named name: T.Type, for indexPath: IndexPath) -> T {
        guard let cell = dequeueReusableCell(withIdentifier: NSStringFromClass(name), for: indexPath) as? T else {
            fatalError(
                "Couldn't find UITableViewCell for \(NSStringFromClass(name)), make sure the cell is registered with table view")
        }
        return cell
    }
    
    func dequeueReusableHeaderFooter<T: UITableViewHeaderFooterView>(named name: T.Type, for indexPath: IndexPath) -> T {
        guard let header = dequeueReusableHeaderFooterView(withIdentifier: NSStringFromClass(name)) as? T else {
            fatalError(
                "Couldn't find UITableViewHeaderFooterView for \(NSStringFromClass(name)), make sure the header or footer is registered with table view")
        }
        return header
    }
}

//
//  UIScrollView+LSExtension.m
//  LPDoorBell
//
//  Created by 付庆平 on 2021/9/13.
//  Copyright © 2021 lancens_app. All rights reserved.
//

#import "UIScrollView+LSExtension.h"
#import "MJRefresh.h"
#import "Aspects.h"

@implementation UIScrollView (LSExtension)

- (void)addHeaderRefresher:(void (^)(void))refreshBlock {
    if (self.mj_header) {
        self.mj_header.refreshingBlock = refreshBlock;
    }else {
        self.mj_header = [MJRefreshNormalHeader headerWithRefreshingBlock:refreshBlock];
    }
}

- (void)addFooterRefresher:(void (^)(void))refreshBlock {
    if (self.mj_footer) {
        self.mj_footer.refreshingBlock = refreshBlock;
    }else {
        self.mj_footer = [MJRefreshAutoNormalFooter footerWithRefreshingBlock:refreshBlock];
        self.mj_footer.hidden = YES;
    }
}

- (void)beginRefreshing:(BOOL)isHeader {
    if (isHeader) {
        [self.mj_header beginRefreshing];
    }else {
        [self.mj_footer beginRefreshing];
    }
}

- (void)beginRefreshing {
    [self beginRefreshing:YES];
}

- (void)endRefreshing:(BOOL)noData {
    if (self.mj_header.isRefreshing) {
        [self.mj_header endRefreshing];
    }
    if (self.mj_footer) {
        [self.mj_footer endRefreshing];
        self.mj_footer.hidden = noData;
    }
}

- (CGFloat)centerOffsetY {
    return [[self associatedValueForKey:_cmd] floatValue];
}

- (void)setCenterOffsetY:(CGFloat)centerOffsetY {
    [self setAssociateValue:@(centerOffsetY) withKey:@selector(centerOffsetY)];
}

- (UIView *)emptyView {
    return [self associatedValueForKey:_cmd];
}

- (void)setEmptyView:(UIView *)emptyView yOffset:(CGFloat)yOffset {
    self.centerOffsetY = yOffset;
    self.emptyView = emptyView;
}

- (void)setEmptyView:(UIView *)emptyView {
    [self.emptyView removeFromSuperview];
    [self setAssociateValue:emptyView withKey:@selector(emptyView)];
    if (emptyView) {
        emptyView.hidden = YES;
        [self addSubview:emptyView];
        [emptyView mas_remakeConstraints:^(MASConstraintMaker *make) {
            make.centerX.mas_equalTo(0);
            make.centerY.mas_equalTo(self.centerOffsetY);
            if (!CGRectEqualToRect(emptyView.frame, CGRectZero)) {
                make.size.mas_equalTo(emptyView.frame.size);
            }
        }];
    }
    SEL reloadSel = NSSelectorFromString(@"reloadData");
    SEL endUpdatesSel = NSSelectorFromString(@"endUpdates");
    MJWeakSelf
    if ([self respondsToSelector:reloadSel]) {
        [self aspect_hookSelector:reloadSel withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> info){
            [weakSelf refreshEmptyViewState];
        } error:nil];
    }
    if ([self respondsToSelector:endUpdatesSel]) {
        [self aspect_hookSelector:endUpdatesSel withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> info){
            [weakSelf refreshEmptyViewState];
        } error:nil];
    }
}

- (NSUInteger)itemsCount {
    int count = 0;
    if ([self isKindOfClass:[UITableView class]]) {
        UITableView *tbv = (UITableView *)self;
        for (int i = 0; i < tbv.numberOfSections; i++) {
            count += [tbv numberOfRowsInSection:i];
        }
        return count;
    }
    if ([self isKindOfClass:[UICollectionView class]]) {
        UICollectionView *cv = (UICollectionView *)self;
        for (int i = 0; i < cv.numberOfSections; i++) {
            count += [cv numberOfItemsInSection:i];
        }
        return count;
    }
    return count;
}

- (void)refreshEmptyViewState {
    self.emptyView.hidden = [self itemsCount] > 0;
}


@end

//
//  UIScrollView+LSExtension.h
//  LPDoorBell
//
//  Created by 付庆平 on 2021/9/13.
//  Copyright © 2021 lancens_app. All rights reserved.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIScrollView (LSExtension)

- (void)addHeaderRefresher:(void(^)(void))refreshBlock;
- (void)addFooterRefresher:(void(^)(void))refreshBlock;
- (void)beginRefreshing;
- (void)beginRefreshing:(BOOL)isHeader;
- (void)endRefreshing:(BOOL)noData;
///占位图

@property (nonatomic, assign) CGFloat centerOffsetY;
@property (nonatomic, strong, nullable) UIView *emptyView;

- (void)setEmptyView:(UIView * _Nullable)emptyView yOffset:(CGFloat)yOffset;

@end

NS_ASSUME_NONNULL_END

// The MIT License (MIT)
//
// Copyright (c) 2015-2016 forkingdog ( https://github.com/forkingdog )
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#import "UINavigationController+FDFullscreenPopGesture.h"
#import <objc/runtime.h>

@interface _FDFullscreenPopGestureRecognizerDelegate : NSObject <UIGestureRecognizerDelegate>

@property (nonatomic, weak) UINavigationController *navigationController;

@end

@implementation _FDFullscreenPopGestureRecognizerDelegate

- (BOOL)gestureRecognizerShouldBegin:(UIPanGestureRecognizer *)gestureRecognizer
{
    // Ignore when no view controller is pushed into the navigation stack.
    if (self.navigationController.viewControllers.count <= 1) {
        return NO;
    }
    
    // Disable when the active view controller doesn't allow interactive pop.
    UIViewController *topViewController = self.navigationController.viewControllers.lastObject;
    if (topViewController.fd_interactivePopDisabled) {
        return NO;
    }
    
    if (topViewController.fd_interactivePopDisabledBlock) {
        CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
        CGPoint location = [gestureRecognizer locationInView:gestureRecognizer.view];
        if (location.x > 40 || translation.x <= 0) return NO;
        if (topViewController.fd_interactivePopDisabledBlock(gestureRecognizer)) return NO;
    }
    
    if (topViewController.fd_ignoreView) {
        CGPoint p = [gestureRecognizer locationInView:topViewController.fd_ignoreView];
        BOOL containsPoint = CGRectContainsPoint(topViewController.fd_ignoreView.bounds, p);
        if (containsPoint) {
            return NO;
        }
    }

    // Ignore pan gesture when the navigation controller is currently in transition.
    if ([[self.navigationController valueForKey:@"_isTransitioning"] boolValue]) {
        return NO;
    }
    
    
    CGPoint translation = [gestureRecognizer translationInView:gestureRecognizer.view];
    CGPoint location = [gestureRecognizer locationInView:gestureRecognizer.view];
    // Prevent calling the handler when the gesture begins in an opposite direction.
    if (location.x > 40 || translation.x <= 0) return NO; /// 左滑最大值为40
    return YES;
}

@end

typedef void (^_FDViewControllerWillAppearInjectBlock)(UIViewController *viewController, BOOL animated);

@interface UIViewController (FDFullscreenPopGesturePrivate)

@property (nonatomic, copy) _FDViewControllerWillAppearInjectBlock fd_willAppearInjectBlock;

@end

@implementation UIViewController (FDFullscreenPopGesturePrivate)

+ (void)load
{
    Method originalMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
    Method swizzledMethod = class_getInstanceMethod(self, @selector(fd_viewWillAppear:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)fd_viewWillAppear:(BOOL)animated
{
    // Forward to primary implementation.
    [self fd_viewWillAppear:animated];
    
    if (self.fd_willAppearInjectBlock) {
        self.fd_willAppearInjectBlock(self, animated);
    }
}

- (_FDViewControllerWillAppearInjectBlock)fd_willAppearInjectBlock
{
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setFd_willAppearInjectBlock:(_FDViewControllerWillAppearInjectBlock)block
{
    objc_setAssociatedObject(self, @selector(fd_willAppearInjectBlock), block, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

@implementation UINavigationController (FDFullscreenPopGesture)

+ (void)load
{
    // Inject "-pushViewController:animated:"
    Method originalMethod = class_getInstanceMethod(self, @selector(pushViewController:animated:));
    Method swizzledMethod = class_getInstanceMethod(self, @selector(fd_pushViewController:animated:));
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

- (void)fd_pushViewController:(UIViewController *)viewController animated:(BOOL)animated
{
    if (![self.interactivePopGestureRecognizer.view.gestureRecognizers containsObject:self.fd_fullscreenPopGestureRecognizer]) {
        
        // Add our own gesture recognizer to where the onboard screen edge pan gesture recognizer is attached to.
        [self.interactivePopGestureRecognizer.view addGestureRecognizer:self.fd_fullscreenPopGestureRecognizer];

        // Forward the gesture events to the private handler of the onboard gesture recognizer.
        NSArray *internalTargets = [self.interactivePopGestureRecognizer valueForKey:@"targets"];
        id internalTarget = [internalTargets.firstObject valueForKey:@"target"];
        SEL internalAction = NSSelectorFromString(@"handleNavigationTransition:");
        self.fd_fullscreenPopGestureRecognizer.delegate = self.fd_popGestureRecognizerDelegate;
        [self.fd_fullscreenPopGestureRecognizer addTarget:internalTarget action:internalAction];
        // Disable the onboard gesture recognizer.
        self.interactivePopGestureRecognizer.enabled = NO;
    }
    
    // Handle perferred navigation bar appearance.
    [self fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:viewController];
    
    // Forward to primary implementation.
    [self fd_pushViewController:viewController animated:animated];
}

- (void)fd_setupViewControllerBasedNavigationBarAppearanceIfNeeded:(UIViewController *)appearingViewController
{
    if (!self.fd_viewControllerBasedNavigationBarAppearanceEnabled) {
        return;
    }
    
    __weak typeof(self) weakSelf = self;
    _FDViewControllerWillAppearInjectBlock block = ^(UIViewController *viewController, BOOL animated) {
        __strong typeof(weakSelf) strongSelf = weakSelf;
        if (strongSelf) {
            [strongSelf setNavigationBarHidden:viewController.fd_prefersNavigationBarHidden animated:animated];
        }
    };
    
    // Setup will appear inject block to appearing view controller.
    // Setup disappearing view controller as well, because not every view controller is added into
    // stack by pushing, maybe by "-setViewControllers:".
    appearingViewController.fd_willAppearInjectBlock = block;
    UIViewController *disappearingViewController = self.viewControllers.lastObject;
    if (disappearingViewController && !disappearingViewController.fd_willAppearInjectBlock) {
        disappearingViewController.fd_willAppearInjectBlock = block;
    }
}

- (_FDFullscreenPopGestureRecognizerDelegate *)fd_popGestureRecognizerDelegate
{
    _FDFullscreenPopGestureRecognizerDelegate *delegate = objc_getAssociatedObject(self, _cmd);

    if (!delegate) {
        delegate = [[_FDFullscreenPopGestureRecognizerDelegate alloc] init];
        delegate.navigationController = self;
        
        objc_setAssociatedObject(self, _cmd, delegate, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return delegate;
}

- (UIPanGestureRecognizer *)fd_fullscreenPopGestureRecognizer
{
    UIPanGestureRecognizer *panGestureRecognizer = objc_getAssociatedObject(self, _cmd);

    if (!panGestureRecognizer) {
        panGestureRecognizer = [[UIPanGestureRecognizer alloc] init];
        panGestureRecognizer.maximumNumberOfTouches = 1;
        
        objc_setAssociatedObject(self, _cmd, panGestureRecognizer, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return panGestureRecognizer;
}

- (BOOL)fd_viewControllerBasedNavigationBarAppearanceEnabled
{
    NSNumber *number = objc_getAssociatedObject(self, _cmd);
    if (number) {
        return number.boolValue;
    }
    self.fd_viewControllerBasedNavigationBarAppearanceEnabled = YES;
    return YES;
}

- (void)setFd_viewControllerBasedNavigationBarAppearanceEnabled:(BOOL)enabled
{
    SEL key = @selector(fd_viewControllerBasedNavigationBarAppearanceEnabled);
    objc_setAssociatedObject(self, key, @(enabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

@implementation UIViewController (FDFullscreenPopGesture)

- (BOOL)fd_interactivePopDisabled
{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

- (void)setFd_interactivePopDisabled:(BOOL)disabled
{
    objc_setAssociatedObject(self, @selector(fd_interactivePopDisabled), @(disabled), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)fd_prefersNavigationBarHidden
{
    return [objc_getAssociatedObject(self, _cmd) boolValue];
}

- (void)setFd_prefersNavigationBarHidden:(BOOL)hidden
{
    objc_setAssociatedObject(self, @selector(fd_prefersNavigationBarHidden), @(hidden), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(UIView *)fd_ignoreView {
    return objc_getAssociatedObject(self, _cmd);
}

-(void)setFd_ignoreView:(UIView *)fd_ignoreView {
    objc_setAssociatedObject(self, @selector(fd_ignoreView), fd_ignoreView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL (^)(void))fd_interactivePopDisabledBlock {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setFd_interactivePopDisabledBlock:(BOOL (^)(void))fd_interactivePopDisabledBlock {
    objc_setAssociatedObject(self, @selector(fd_interactivePopDisabledBlock), fd_interactivePopDisabledBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

// The MIT License (MIT)
//
// Copyright (c) 2015-2016 forkingdog ( https://github.com/forkingdog )
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

#import <UIKit/UIKit.h>

/// "UINavigation+FDFullscreenPopGesture" extends UINavigationController's swipe-
/// to-pop behavior in iOS 7+ by supporting fullscreen pan gesture. Instead of
/// screen edge, you can now swipe from any place on the screen and the onboard
/// interactive pop transition works seamlessly.
///
/// Adding the implementation file of this category to your target will
/// automatically patch UINavigationController with this feature.
@interface UINavigationController (FDFullscreenPopGesture)

/// The gesture recognizer that actually handles interactive pop.
@property (nonatomic, strong, readonly) UIPanGestureRecognizer *fd_fullscreenPopGestureRecognizer;

/// A view controller is able to control navigation bar's appearance by itself,
/// rather than a global way, checking "fd_prefersNavigationBarHidden" property.
/// Default to YES, disable it if you don't want so.
@property (nonatomic, assign) BOOL fd_viewControllerBasedNavigationBarAppearanceEnabled;

@end

/// Allows any view controller to disable interactive pop gesture, which might
/// be necessary when the view controller itself handles pan gesture in some
/// cases.
@interface UIViewController (FDFullscreenPopGesture)

@property (nonatomic, copy) BOOL(^fd_interactivePopDisabledBlock)(UIPanGestureRecognizer *pan);

/// Whether the interactive pop gesture is disabled when contained in a navigation
/// stack.
@property (nonatomic, assign) BOOL fd_interactivePopDisabled;

/// Indicate this view controller prefers its navigation bar hidden or not,
/// checked when view controller based navigation bar's appearance is enabled.
/// Default to NO, bars are more likely to show.
@property (nonatomic, assign) BOOL fd_prefersNavigationBarHidden;


///新增属性，用于指定视图禁止触发手势
@property (nonatomic, strong, nullable) UIView *fd_ignoreView;

@end

//
//  UILabel+LSExtension.m
//  LPDoorBell
//
//  Created by Champion Fu on 2021/4/3.
//  Copyright © 2021 lancens_app. All rights reserved.
//

#import "UILabel+LSExtension.h"


@implementation UILabel (LSExtension)


- (CGFloat)textHeightForWidth:(CGFloat)width {
    if ([self.attributedText length]) {
        return [self.attributedText heightForWidth:width];
    }
    return [self.text heightForFont:self.font width:width];
}

- (CGSize)textSizeForWidth:(CGFloat)width {
    return CGSizeMake([self textWidth], [self textHeightForWidth:width]);
}

- (CGFloat)textWidth {
    if ([self.attributedText length]) {
        return [self.attributedText width];
    }
    return [self.text widthForFont:self.font];
}

+ (instancetype)labelWithText:(id)text font:(id)font color:(UIColor *)color {
    UILabel *l = [[UILabel alloc] init];
    if (APISAStr(text)) {
        l.attributedText = text;
    }else if (APISStr(text)) {
        l.text = text;
        if (nil != font) l.font = [UIFont IDFont:font];
        if (nil != color) l.textColor = color;
    }else {
        if (nil != font) l.font = [UIFont IDFont:font];
        if (nil != color) l.textColor = color;
    }
    return l;
}


+ (instancetype)labelWithFont:(id)font color:(UIColor *)color {
    return [self labelWithText:nil font:font color:color];
}

+ (instancetype)labelWithText:(id)text {
    return [self labelWithText:text font:nil color:nil];
}

- (void)setString:(id)string {
    if (nil == string || [string isKindOfClass:[NSString class]]) {
        self.text = string;
    }else {
        self.attributedText = string;
    }
}

- (id)string {
    if (self.attributedText) return self.attributedText;
    return self.text;
}

- (void)scale:(CGFloat)scale lines:(NSUInteger)lines {
    self.minimumScaleFactor = scale;
    self.adjustsFontSizeToFitWidth = YES;
    self.numberOfLines = lines;
}

+ (instancetype)popTitleLabel:(id)title {
    let label = [self labelWithText:title font:@17.6 color:APLabel];
    label.textAlignment = 1;
    [label scale:0.9 lines:2];
    [label addLine:1];
    return label;
}

@end
//
//  UILabel+LSExtension.h
//  LPDoorBell
//
//  Created by Champion Fu on 2021/4/3.
//  Copyright © 2021 lancens_app. All rights reserved.
//

#import <UIKit/UIKit.h>
#import "CAGradientLayer+APExtension.h"

NS_ASSUME_NONNULL_BEGIN

@interface UILabel (LSExtension)

+ (instancetype)labelWithText:(nullable id)text font:(nullable id)font color:(nullable UIColor *)color;
+ (instancetype)labelWithText:(nullable id)text;
+ (instancetype)labelWithFont:(id)font color:(UIColor *)color;

@property (nonatomic, copy, nullable) id string;

- (CGFloat)textHeightForWidth:(CGFloat)width NS_SWIFT_NAME(textHeight(_:));

- (CGFloat)textWidth;

- (CGSize)textSizeForWidth:(CGFloat)width NS_SWIFT_NAME(textSize(_:));

- (void)scale:(CGFloat)scale lines:(NSUInteger)lines;

+ (instancetype)popTitleLabel:(id)title;

@end

NS_ASSUME_NONNULL_END

//
//  UILabel+APExtension.swift
//  FuweiCloud
//
//  Created by michael on 2024/5/29.
//

import UIKit

extension UILabel {
    convenience init(text: Any? = nil, font: Any? = nil, color: UIColor? = nil, lines: Int = 1, aligment: NSTextAlignment = .left) {
        self.init(text: text, font: font, color: color)
        numberOfLines = lines
        textAlignment = aligment
    }
}

//
//  UIImageView+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/3/24.
//

#import "UIImageView+APExtension.h"

@implementation UIImageView (APExtension)
+ (instancetype)imageNamed:(NSString *)imageName {
    return [[self alloc] initWithImage:[UIImage imageNamed:imageName]];
}
+ (instancetype)imageNamed:(NSString *)imageName contentMode:(UIViewContentMode)contentMode {
    UIImageView *imageView = [[self alloc] initWithImage:[UIImage imageNamed:imageName]];
    imageView.contentMode = contentMode;
    return imageView;
}

+ (instancetype)imageWithContentMode:(UIViewContentMode)contentMode {
    return [self imageNamed:nil contentMode:contentMode];
}

+ (instancetype)systemNamed:(NSString *)systemName {
    return [[self alloc] initWithImage:[UIImage systemImageNamed:systemName]];
}

+ (instancetype)viewWithImage:(UIImage *)image {
    return [[self alloc] initWithImage:image];
}

- (void)ap_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder {
    [self ap_setImageWithURL:url placeholderImage:placeholder rotation:0 filterCacheKey:YES];
}

- (void)ap_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder rotation:(CGFloat)rotation {
    [self ap_setImageWithURL:url placeholderImage:placeholder rotation:rotation filterCacheKey:YES];
}

- (void)ap_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder rotation:(CGFloat)rotation filterCacheKey:(BOOL)filerCacheKey {
    NSMutableDictionary<SDWebImageContextOption, id> *ctx = [NSMutableDictionary dictionary];
    if (url && ((int)rotation % 360) != 0) {
        let rotationTransformer = [SDImageRotationTransformer transformerWithAngle:DegreesToRadians(-rotation) fitSize:YES];
        [ctx addEntriesFromDictionary:@{SDWebImageContextImageTransformer:rotationTransformer}];
    }
    if (url && filerCacheKey) {
        let cacheKeyFilter = [SDWebImageCacheKeyFilter cacheKeyFilterWithBlock:^NSString * _Nullable(NSURL * _Nonnull url) {
            let query = [url query];
            NSString *urlStr = url.absoluteString;
            if ([query length]) {
                return [urlStr stringByReplacingOccurrencesOfString:query withString:@""];
            }else {
                return urlStr;
            }
        }];
        [ctx addEntriesFromDictionary:@{SDWebImageContextCacheKeyFilter:cacheKeyFilter}];
    }
    [self sd_setImageWithURL:url placeholderImage:placeholder options:SDWebImageRetryFailed context:[ctx copy]];
}

- (void)downloadImageWithUrl:(nullable NSString *)url placeholder:(nullable NSString *)placeholder{
    [self sd_setImageWithURL:[NSURL URLWithString:url] placeholderImage:placeholder.length>0?APImageNamed(placeholder):nil options:SDWebImageRetryFailed|SDWebImageLowPriority];
}

+ (instancetype)APLoadingView {
    let view = [self viewWithImage:APImageNamed(@"toast_loading")];
    [view rotateImage:YES];
    return view;
}

- (void)rotateImage:(BOOL)animated {
    if ([self.layer animationForKey:@"rotationAnim"] && animated) return;
    if (animated) {
        CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
        anim.duration = 0.618f;
        anim.repeatCount = HUGE_VALF;
        anim.fromValue = @0;
        anim.toValue = @(M_PI*2);
        anim.removedOnCompletion = NO;
        [self.layer addAnimation:anim forKey:@"rotationAnim"];
    }else {
        [self.layer removeAllAnimations];
    }
}

@end

//
//  UIImageView+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/3/24.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

#define APImageViewNamed(_n_) [UIImageView imageNamed:_n_]
#define APImageView(_n_) [UIImageView viewWithImage:_n_]
@interface UIImageView (APExtension)

+ (instancetype)imageNamed:(nullable NSString *)imageName NS_SWIFT_NAME(init(named:));
+ (instancetype)imageWithContentMode:(UIViewContentMode)contentMode NS_SWIFT_NAME(init(mode:));
+ (instancetype)imageNamed:(nullable NSString *)imageName contentMode:(UIViewContentMode)contentMode NS_SWIFT_NAME(init(named:mode:));

+ (instancetype)systemNamed:(NSString *)systemName NS_SWIFT_NAME(init(systemNamed:));

+ (instancetype)viewWithImage:(UIImage *)image;

+ (instancetype)APLoadingView;

- (void)rotateImage:(BOOL)animated;

- (void)ap_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder;
- (void)ap_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder rotation:(CGFloat)rotation;
- (void)ap_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder rotation:(CGFloat)rotation filterCacheKey:(BOOL)filerCacheKey;

/// 简易图片下载 url: 下载路径  placeholder: 占位图片
- (void)downloadImageWithUrl:(nullable NSString *)url placeholder:(nullable NSString *)placeholder;

@end

NS_ASSUME_NONNULL_END

//
//  UIImage+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/4/20.
//

#import "UIImage+APExtension.h"

@implementation UIImage (APExtension)

- (CGFloat)whRatio {
    if (self.size.height == 0) return 0;
    return self.size.width / self.size.height;
}

+ (instancetype)imageWithColors:(NSArray<UIColor *> *)colors size:(CGSize)size locations:(NSArray *)locations type:(APGradientImageType)type {
    NSMutableArray *cgColors = [NSMutableArray array];
    for(UIColor *c in colors) {
        [cgColors addObject:(id)c.CGColor];
    }
    CGFloat *locs = NULL;
    if (colors.count) {
        locs = (CGFloat *)malloc(cgColors.count*sizeof(CGFloat));
        for (int i = 0; i < locations.count; i++) {
            locs[i] = [locations[i] floatValue];
        }
    }
    UIGraphicsBeginImageContextWithOptions(size, YES, 1);
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextSaveGState(context);
    CGColorSpaceRef colorSpace = CGColorGetColorSpace([[colors lastObject] CGColor]);
    CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (CFArrayRef)cgColors, locs);
    CGPoint start;
    CGPoint end;
    switch (type) {
        case APGradientImageType_TopToBottom:
        {
            start = CGPointMake(size.width/2, 0.0);
            end = CGPointMake(size.width/2, size.height);
        }
            break;
        case APGradientImageType_LeftToRight:
        {
            start = CGPointMake(0.0, size.height/2);
            end = CGPointMake(size.width, size.height/2);
        }
            break;
        case APGradientImageType_LeftTopToRightBottom:
        {
            start = CGPointMake(0.0, 0.0);
            end = CGPointMake(size.width, size.height);
        }
            break;
        case APGradientImageType_LeftBottomToRightTop:
        {
            start = CGPointMake(0.0, size.height);
            end = CGPointMake(size.width, 0.0);
        }
            break;
        default:
            break;
    }
    CGContextDrawLinearGradient(context, gradient, start, end, kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    CGGradientRelease(gradient);
    CGContextRestoreGState(context);
    CGColorSpaceRelease(colorSpace);
    UIGraphicsEndImageContext();
    if (locs) free(locs);
    return image;
}

+ (instancetype)imageWithColors:(NSArray<UIColor *> *)colors size:(CGSize)size type:(APGradientImageType)type {
    NSMutableArray *locations = [NSMutableArray arrayWithCapacity:colors.count];
    if (colors.count == 1) {
        [locations addObject:@0];
    }else {
        CGFloat phrase = 1 / (CGFloat)(colors.count - 1);
        for (int i = 0; i < colors.count; i++) {
            [locations addObject:@(phrase*i)];
        }
    }
    return [self imageWithColors:colors size:size locations:locations type:type];
}

+ (instancetype)imageWithColors:(NSArray<UIColor *> *)colors type:(APGradientImageType)type {
    return [self imageWithColors:colors size:CGSizeMake(10, 10) type:type];
}

+ (instancetype)imageWithColors:(NSArray<UIColor *> *)colors size:(CGSize)size {
    return [self imageWithColors:colors size:size type:APGradientImageType_LeftToRight];
}

+ (instancetype)imageWithColors:(NSArray<UIColor *> *)colors {
    return [self imageWithColors:colors size:CGSizeMake(10, 10) type:APGradientImageType_LeftToRight];
}

+ (instancetype)themeGradient {
    return [self imageWithColors:@[APThemeDarkColor,APThemeLightColor]];
}

+ (instancetype)theme1Gradient {
    NSArray *colors = nil;
    if (FW_ARPHA_FLAG) {
        colors = @[APThemeDarkColor,APThemeLightColor,APThemeDarkColor];
    }else if (FW_CLDBUS_FLAG) {
        colors = @[APThemeLightColor,APThemeDarkColor];
    }else if (FW_LNDU_FLAG) {
        colors = @[APThemeLightColor,APThemeDarkColor];
    }
    return [self imageWithColors:colors size:CGSizeMake(40, 40)];
}

+ (instancetype)knobImageWithRadius:(CGFloat)radius space:(CGFloat)space color:(UIColor *)color alpha:(CGFloat)alpha {
    return [UIImage imageWithSize:CGSizeMake(radius, radius) drawBlock:^(CGContextRef  _Nonnull context) {
        let rect = (CGRect){0, 0, radius, radius};
        CGContextSetFillColorWithColor(context, APAlpha(color, alpha).CGColor);
        CGContextFillEllipseInRect(context, rect);
        CGContextSetFillColorWithColor(context, color.CGColor);
        CGContextFillEllipseInRect(context, CGRectInset(rect, space, space));
    }];
}

@end

//
//  UIImage+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/4/20.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, APGradientImageType) {
    APGradientImageType_LeftToRight,
    APGradientImageType_TopToBottom,
    APGradientImageType_LeftTopToRightBottom,
    APGradientImageType_LeftBottomToRightTop
};

#define APImageNamed(_n_) [UIImage imageNamed:_n_]
@interface UIImage (APExtension)

@property (nonatomic, assign, readonly) CGFloat whRatio;

+ (instancetype)imageWithColors:(nullable NSArray<UIColor *> *)colors size:(CGSize)size locations:(NSArray *)locations type:(APGradientImageType)type;

+ (instancetype)imageWithColors:(nullable NSArray<UIColor *> *)colors size:(CGSize)size type:(APGradientImageType)type;

+ (instancetype)imageWithColors:(nullable NSArray<UIColor *> *)colors size:(CGSize)size;

+ (instancetype)imageWithColors:(nullable NSArray<UIColor *> *)colors type:(APGradientImageType)type;

+ (instancetype)imageWithColors:(nullable NSArray<UIColor *> *)colors;

+ (instancetype)themeGradient;

+ (instancetype)theme1Gradient;

+ (instancetype)knobImageWithRadius:(CGFloat)radius space:(CGFloat)space color:(UIColor *)color alpha:(CGFloat)alpha;

@end

NS_ASSUME_NONNULL_END

//
//  UIFont+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/4/27.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIFont (APExtension)


/*
 1、font is UIFont
 2、font is @14.5 = [UIFont systemFontOfSize:14.5]
 3、font is @-14.5 = [UIFont systemFontOfSize:14.5 weight:UIFontWeightSemibold];
 */
/// NSNumber: 小于0: 中粗； 大于0: int型：常规  float型：小数点最后一位作为字重 medium：number.5  Semibold：number.6
+ (UIFont *)IDFont:(id)font;



/* 索引从1开始： UIFontWeightUltraLight：.1  UIFontWeightThin：.2
 UIFontWeight fws[9] = {UIFontWeightUltraLight,UIFontWeightThin,UIFontWeightLight,
 UIFontWeightRegular,UIFontWeightMedium,UIFontWeightSemibold,UIFontWeightBold,UIFontWeightHeavy,UIFontWeightBlack};
 等价于
 float fws[9] = {-0.8,-0.6,-0.4,0.0,0.23,0.30,0.4,0.56,0.62};
 num含义
 1、17或17.0表示fontSize = 17, fontWeight = regular（常规）和17.4等价
 2、-17.12表示fontSize = 17.12, fontWeight = semibold（中粗）
 3、17.2表示fontSize = 17, fontWeight = thin, 即fws[1]
 4、17.25表示fontSize = 17.2, fontWeight = medium,  即fws[4]
 */
+ (UIFont *)numFont:(float)num;

@end

NS_ASSUME_NONNULL_END

//
//  UIFont+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/4/27.
//

#import "UIFont+APExtension.h"

@implementation UIFont (APExtension)

+ (UIFont *)IDFont:(id)font {
    UIFont *f = [UIFont systemFontOfSize:17];
    if ([font isKindOfClass:[UIFont class]]) {
        f = font;
    }else if ([font isKindOfClass:[NSNumber class]]) {
        return [self numFont:[font floatValue]];
    }
    return f;
}

+ (UIFont *)numFont:(float)num {
    if (num < 0) {
        return [UIFont systemFontOfSize:-num weight:UIFontWeightSemibold];
    }else {
        NSString *fontStr = [NSString stringWithFormat:@"%g",num];
        if ([fontStr containsString:@"."]) {
            UIFontWeight fws[] = {UIFontWeightUltraLight,UIFontWeightThin,UIFontWeightLight,
                UIFontWeightRegular,UIFontWeightMedium,UIFontWeightSemibold,UIFontWeightBold,UIFontWeightHeavy,UIFontWeightBlack};
            int weight = [[fontStr substringFromIndex:fontStr.length - 1] intValue];
            weight = YY_CLAMP(weight - 1, 0, 8);
            float size = [[fontStr substringToIndex:fontStr.length - 1] floatValue];
            return [UIFont systemFontOfSize:size weight:fws[weight]];
        }else {
            return [UIFont systemFontOfSize:num];
        }
    }
}

@end

//
//  APExtension.m
//  FuweiCloud
//
//  Created by michael on 2024/1/5.
//

#import "UIColor+APExtension.h"

#define APMainBundleColorIMP(clr) + (UIColor *)clr {     \
    return [UIColor colorNamed:@#clr];                   \
}                                                        \

@implementation UIColor(APExtension)

- (id)cgColor {
    return (__bridge  id _Nullable)self.CGColor;
}


+ (instancetype)dynamicColor:(UIColor *)light dark:(UIColor *)dark {
    return [UIColor colorWithDynamicProvider:^UIColor * _Nonnull(UITraitCollection * _Nonnull traitCollection) {
        if (traitCollection.userInterfaceStyle == UIUserInterfaceStyleDark) return dark;
        return light;
    }];
}

APMainBundleColorIMP(background)
APMainBundleColorIMP(inputBackground)
APMainBundleColorIMP(popView)
APMainBundleColorIMP(loginGold0)
APMainBundleColorIMP(loginGold1)
APMainBundleColorIMP(loginGold2)

+ (UIColor *)themeTextColor {
#ifdef ARPHA_PLUS
    return APHEX(#F4D19F);
#elif CLDBUS
    return APHEX(#0078FA);
#else
    return APHEX(#FD5050);
#endif
}

+ (UIColor *)themeLightColor {
#ifdef ARPHA_PLUS
    return APHEX(#F5D6A6);
#elif CLDBUS
    return APHEX(#00B4FF);
#else
    return APHEX(#FF834F);
#endif
}
+ (UIColor *)themeDarkColor {
#ifdef ARPHA_PLUS
    return APHEX(#C9A063);
#elif CLDBUS
    return APHEX(#0078FA);
#else
    return APHEX(#F02929);
#endif
}

+ (UIColor *)themeBackgroundColor { return APRGB(244, 245, 245); }
+ (UIColor *)lineColor { return APGray2(E2); }
+ (UIColor *)buttonColor { return APGray2(4D); }
+ (UIColor *)backgroundColor { return [UIColor systemBackgroundColor]; }
+ (UIColor *)background1Color { return APDYN(APGray2(f2),APGray1(0)); }
+ (UIColor *)background2Color { return APDYN(APGray1(f),APGray2(1c)); }
+ (UIColor *)inputColor { return APDYN(APGray2(f5),APGray2(1c)); }
+ (UIColor *)controlColor { return APDYN(APGray2(f2),APGray2(39)); }
+ (UIColor *)errorColor { return APHEX(#C63132); }
+ (UIColor *)label1Color { return APDYN(APGray1(6),APGray1(d)); }
+ (UIColor *)label2Color { return APGray1(9); }
+ (UIColor *)apSeperatorColor { return APDYN(APGray2(D7), APGray2(52)); }
+ (UIColor *)apSeperatorColor_37 { return APHEX(#373737); }

@end

//
//  APExtension.h
//  FuweiCloud
//
//  Created by michael on 2024/1/5.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

#define APMainBundleColor(clr) @property (nonatomic, copy, class, readonly) UIColor *clr;
@interface UIColor(APExtension)

@property (nonatomic, readonly, nullable) id cgColor NS_SWIFT_UNAVAILABLE("only for objc");


+ (instancetype)dynamicColor:(UIColor *)light dark:(UIColor *)dark;

APMainBundleColor(themeTextColor)
APMainBundleColor(background)
APMainBundleColor(inputBackground)
APMainBundleColor(popView)

APMainBundleColor(themeLightColor)
APMainBundleColor(themeDarkColor)
APMainBundleColor(themeBackgroundColor)
APMainBundleColor(backgroundColor)
APMainBundleColor(background1Color) // 主题黑色
APMainBundleColor(background2Color) // cell 灰色
APMainBundleColor(inputColor)
APMainBundleColor(controlColor)
APMainBundleColor(errorColor)
APMainBundleColor(gold1Color)
APMainBundleColor(gold2Color)
APMainBundleColor(lineColor)
APMainBundleColor(buttonColor)
APMainBundleColor(apSeperatorColor) // 分割线 D7 -> 52
APMainBundleColor(apSeperatorColor_37) // 分割线 373737
APMainBundleColor(label1Color)
APMainBundleColor(label2Color)

@end

NS_ASSUME_NONNULL_END

//
//  UICollectionView+APExtension.swift
//  FuweiCloud
//
//  Created by michael on 2024/6/6.
//

import UIKit

extension UICollectionView {

    func dequeueReusableCell<T: UICollectionViewCell>(named name: T.Type, for indexPath: IndexPath) -> T {
        guard let cell = dequeueReusableCell(withReuseIdentifier: NSStringFromClass(name), for: indexPath) as? T else {
            fatalError(
                "Couldn't find UICollectionViewCell for \(NSStringFromClass(name)), make sure the cell is registered with collection view")
        }
        return cell
    }
    
    func dequeueReusableHeaderFooter<T: UICollectionReusableView>(named name: T.Type, isHeader: Bool, for indexPath: IndexPath) -> T {
        guard let view = dequeueReusableSupplementaryView( ofKind: isHeader ?UICollectionView.elementKindSectionHeader:UICollectionView.elementKindSectionFooter, withReuseIdentifier: NSStringFromClass(name), for: indexPath) as? T else {
            fatalError(
                "Couldn't find UICollectionReusableView for \(NSStringFromClass(name)), make sure the view is registered with collection view")
        }
        return view
    }
}

//
//  UICollectionView+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/4/19.
//

#import "UICollectionView+APExtension.h"

@implementation UICollectionView (APExtension)

+ (instancetype)collectionViewWithDelegate:(id<UICollectionViewGeneralDelegate>)delegate cell:(Class)cell itemSize:(CGSize)itemSize rowColumn:(CGSize)rowColumn {
    return [self collectionViewWithDelegate:delegate cell:(Class)cell itemSize:itemSize rowColumn:rowColumn sectionInset:UIEdgeInsetsZero direction:0];
}

+ (instancetype)collectionViewWithDelegate:(id<UICollectionViewGeneralDelegate>)delegate cell:(Class)cell itemSize:(CGSize)itemSize rowColumn:(CGSize)rowColumn sectionInset:(UIEdgeInsets)inset {
    return [self collectionViewWithDelegate:delegate cell:cell itemSize:itemSize rowColumn:rowColumn sectionInset:inset direction:0];
}

+ (instancetype)collectionViewWithDelegate:(id<UICollectionViewGeneralDelegate>)delegate cell:(Class)cell itemSize:(CGSize)itemSize rowColumn:(CGSize)rowColumn sectionInset:(UIEdgeInsets)inset direction:(NSUInteger)direction {
    UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
    layout.scrollDirection = direction;
    layout.itemSize = itemSize;
    layout.minimumLineSpacing = rowColumn.height;
    layout.minimumInteritemSpacing = rowColumn.width;
    layout.sectionInset = inset;
    UICollectionView *collectionView = [[self alloc] initWithFrame:CGRectZero collectionViewLayout:layout];
    [collectionView registerClass:cell?[cell class]:[UICollectionViewCell class] forCellWithReuseIdentifier:cell?[cell className]:[UICollectionViewCell className]];
    collectionView.delegate = delegate;
    collectionView.dataSource = delegate;
    return collectionView;
}

- (void)registerHeader:(nullable Class)header footer:(nullable Class)footer {
    if (header) {
        NSAssert([header isSubclassOfClass:[UICollectionReusableView class]], @"header's superclass is not UICollectionReusableView");
        [self registerClass:header forSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:[header className]];
    }
    if (footer) {
        NSAssert([footer isSubclassOfClass:[UICollectionReusableView class]], @"footer's superclass is not UICollectionReusableView");
        [self registerClass:footer forSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:[footer className]];
    }
}

- (void)registerCells:(NSArray<Class> *)cells {
    for (Class cell in cells) {
        [self registerClass:cell forCellWithReuseIdentifier:[cell className]];
    }
}

- (UICollectionViewCell *)dequeueReusableCell:(Class)cell forIndexPath:(NSIndexPath *)indexPath {
    return [self dequeueReusableCellWithReuseIdentifier:[cell className] forIndexPath:indexPath];
}

- (UICollectionReusableView *)dequeueReusableHeader:(Class)header forIndexPath:(NSIndexPath *)indexPath {
    return [self dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionHeader withReuseIdentifier:[header className] forIndexPath:indexPath];
}

- (UICollectionReusableView *)dequeueReusableFooter:(Class)footer forIndexPath:(NSIndexPath *)indexPath {
    return [self dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter withReuseIdentifier:[footer className] forIndexPath:indexPath];
}

@end

//
//  UICollectionView+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/4/19.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@protocol UICollectionViewGeneralDelegate <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@end

@interface UICollectionView (APExtension)

+ (instancetype)collectionViewWithDelegate:(id<UICollectionViewGeneralDelegate>)delegate cell:(nullable Class)cell itemSize:(CGSize)itemSize rowColumn:(CGSize)rowColumn;
+ (instancetype)collectionViewWithDelegate:(id<UICollectionViewGeneralDelegate>)delegate cell:(nullable Class)cell itemSize:(CGSize)itemSize rowColumn:(CGSize)rowColumn sectionInset:(UIEdgeInsets)inset;
+ (instancetype)collectionViewWithDelegate:(id<UICollectionViewGeneralDelegate>)delegate cell:(nullable Class)cell itemSize:(CGSize)itemSize rowColumn:(CGSize)rowColumn sectionInset:(UIEdgeInsets)inset direction:(NSUInteger)direction;

- (void)registerCells:(NSArray<Class> *)cells;
- (void)registerHeader:(nullable Class)header footer:(nullable Class)footer;

- (UICollectionViewCell *)dequeueReusableCell:(Class)cell forIndexPath:(NSIndexPath *)indexPath;
- (UICollectionReusableView *)dequeueReusableHeader:(Class)header forIndexPath:(NSIndexPath *)indexPath;
- (UICollectionReusableView *)dequeueReusableFooter:(Class)footer forIndexPath:(NSIndexPath *)indexPath;

@end

NS_ASSUME_NONNULL_END

//
//  UIButton+KJContentLayout.m
//  CategoryDemo
//
//  Created by 77。 on 2018/7/7.
//  https://github.com/YangKJ/KJCategories

#import "UIButton+KJContentLayout.h"
#import <objc/runtime.h>

@implementation UIButton (KJContentLayout)

/// 设置图文混排，默认图文边界间距5px
/// @param layoutStyle 图文混排样式
/// @param padding 图文间距
- (void)kj_contentLayout:(KJButtonContentLayoutStyle)layoutStyle padding:(CGFloat)padding{
    [self kj_contentLayout:layoutStyle padding:padding periphery:5];
}

/// 设置图文混排
/// @param layoutStyle 图文混排样式
/// @param padding 图文间距
/// @param periphery 图文边界的间距
- (void)kj_contentLayout:(KJButtonContentLayoutStyle)layoutStyle
                 padding:(CGFloat)padding
               periphery:(CGFloat)periphery{
    self.imageView.contentMode = UIViewContentModeScaleAspectFit;
    CGFloat imageWith   = self.imageView.image.size.width;
    CGFloat imageHeight = self.imageView.image.size.height;
//    CGSize size = [self.titleLabel.text sizeWithAttributes:@{NSFontAttributeName:self.titleLabel.font}];
    CGFloat labelWidth = 0, labelHeight = 0;
    if ([UIDevice currentDevice].systemVersion.floatValue >= 8.0) {
        labelWidth  = self.titleLabel.intrinsicContentSize.width;
        labelHeight = self.titleLabel.intrinsicContentSize.height;
        CGSize size = [self.titleLabel sizeThatFits:CGSizeMake(MAXFLOAT, MAXFLOAT)];
        labelWidth = size.width;
        labelHeight = size.height;
    } else {
        labelWidth  = self.titleLabel.frame.size.width;
        labelHeight = self.titleLabel.frame.size.height;
    }
    UIEdgeInsets imageEdge = UIEdgeInsetsZero;
    UIEdgeInsets titleEdge = UIEdgeInsetsZero;
    switch (layoutStyle) {
        case KJButtonContentLayoutStyleNormal:{
            titleEdge = UIEdgeInsetsMake(0, padding, 0, 0);
            imageEdge = UIEdgeInsetsMake(0, 0, 0, padding);
            self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
        } break;
        case KJButtonContentLayoutStyleCenterImageRight:{
            titleEdge = UIEdgeInsetsMake(0, -imageWith - padding, 0, imageWith);
            imageEdge = UIEdgeInsetsMake(0, labelWidth + padding, 0, -labelWidth);
            self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
        } break;
        case KJButtonContentLayoutStyleCenterImageTop:{
            titleEdge = UIEdgeInsetsMake(0, -imageWith, -imageHeight - padding, 0);
            imageEdge = UIEdgeInsetsMake(-labelHeight - padding, 0, 0, -labelWidth);
            self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
        } break;
        case KJButtonContentLayoutStyleCenterImageBottom:{
            titleEdge = UIEdgeInsetsMake(-imageHeight - padding, -imageWith, 0, 0);
            imageEdge = UIEdgeInsetsMake(0, 0, -labelHeight - padding, -labelWidth);
            self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter;
        } break;
        case KJButtonContentLayoutStyleLeftImageLeft:{
            titleEdge = UIEdgeInsetsMake(0, padding + periphery, 0, 0);
            imageEdge = UIEdgeInsetsMake(0, periphery, 0, 0);
            self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
        } break;
        case KJButtonContentLayoutStyleLeftImageRight:{
            titleEdge = UIEdgeInsetsMake(0, -imageWith + periphery, 0, 0);
            imageEdge = UIEdgeInsetsMake(0, labelWidth + padding + periphery, 0, 0);
            self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
        } break;
        case KJButtonContentLayoutStyleRightImageLeft:{
            imageEdge = UIEdgeInsetsMake(0, 0, 0, padding + periphery);
            titleEdge = UIEdgeInsetsMake(0, 0, 0, periphery);
            self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight;
        } break;
        case KJButtonContentLayoutStyleRightImageRight:{
            titleEdge = UIEdgeInsetsMake(0, -self.frame.size.width / 2, 0, imageWith + padding + periphery);
            imageEdge = UIEdgeInsetsMake(0, 0, 0, -labelWidth + periphery);
            self.contentHorizontalAlignment = UIControlContentHorizontalAlignmentRight;
        } break;
        default:break;
    }
    self.imageEdgeInsets = imageEdge;
    self.titleEdgeInsets = titleEdge;
    [self setNeedsDisplay];
}

#pragma mark - associated

- (NSInteger)layoutType{
    return [objc_getAssociatedObject(self, @selector(layoutType)) integerValue];;
}
- (void)setLayoutType:(NSInteger)layoutType{
    objc_setAssociatedObject(self, @selector(layoutType), @(layoutType), OBJC_ASSOCIATION_ASSIGN);
    [self kj_contentLayout:layoutType padding:self.padding periphery:self.periphery];
}
- (CGFloat)padding{
    return [objc_getAssociatedObject(self, @selector(padding)) floatValue];
}
- (void)setPadding:(CGFloat)padding{
    objc_setAssociatedObject(self, @selector(padding), @(padding), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self kj_contentLayout:self.layoutType padding:padding periphery:self.periphery];
}
- (CGFloat)periphery{
    return [objc_getAssociatedObject(self, @selector(periphery)) floatValue];
}
- (void)setPeriphery:(CGFloat)periphery{
    objc_setAssociatedObject(self, @selector(periphery), @(periphery), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    [self kj_contentLayout:self.layoutType padding:self.padding periphery:periphery];
}

@end

//
//  UIButton+KJContentLayout.h
//  CategoryDemo
//
//  Created by 77。 on 2018/7/7.
//  https://github.com/YangKJ/KJCategories

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSInteger, KJButtonContentLayoutStyle) {
    KJButtonContentLayoutStyleNormal = 0,       // 内容居中-图左文右
    KJButtonContentLayoutStyleCenterImageRight, // 内容居中-图右文左
    KJButtonContentLayoutStyleCenterImageTop,   // 内容居中-图上文下
    KJButtonContentLayoutStyleCenterImageBottom,// 内容居中-图下文上
    KJButtonContentLayoutStyleLeftImageLeft,    // 内容居左-图左文右
    KJButtonContentLayoutStyleLeftImageRight,   // 内容居左-图右文左
    KJButtonContentLayoutStyleRightImageLeft,   // 内容居右-图左文右
    KJButtonContentLayoutStyleRightImageRight,  // 内容居右-图右文左
};
IB_DESIGNABLE
@interface UIButton (KJContentLayout)

/// Graphic style
@property (nonatomic, assign) IBInspectable NSInteger layoutType;
/// Picture and text spacing, the default is 0px
@property (nonatomic, assign) IBInspectable CGFloat padding;
/// The spacing between the graphic and text borders, the default is 5px
@property (nonatomic, assign) IBInspectable CGFloat periphery;

/// Set graphics and text mixing, the default border spacing between graphics and text is 5px
/// @param layoutStyle Graphic and text mixed style
/// @param padding Image and text spacing
- (void)kj_contentLayout:(KJButtonContentLayoutStyle)layoutStyle
                 padding:(CGFloat)padding;

/// Set image and text mixing
/// FIXME: There is a problem with this writing that it will break the automatic layout of the button
/// @param layoutStyle Graphic and text mixed style
/// @param padding Image and text spacing
/// @param periphery The distance between the graphic borders
- (void)kj_contentLayout:(KJButtonContentLayoutStyle)layoutStyle
                 padding:(CGFloat)padding
               periphery:(CGFloat)periphery;

@end

NS_ASSUME_NONNULL_END

//
//  UIButton+ContentLayout.m
//  RMReader
//
//  Created by bx on 12/5/19.
//  Copyright © 2019 bx. All rights reserved.
//

#import "UIButton+ContentLayout.h"
#import <objc/runtime.h>

static void *infoDicKey = &infoDicKey;
@interface UIButton ()

@property (strong, nonatomic) NSMutableDictionary *infoDic;
@property (assign, nonatomic) UIButtonLayoutType type;
@property (assign, nonatomic) CGFloat space;
@property (assign, nonatomic) BOOL support;

@end


@implementation UIButton (ContentLayout)
- (void)setLabelLeftImageRight {
    CGFloat labelWidth = self.titleLabel.intrinsicContentSize.width;
    CGFloat imageWith = self.imageView.frame.size.width;
    self.imageEdgeInsets = UIEdgeInsetsMake(0, labelWidth+2, 0, -labelWidth -2);
    self.titleEdgeInsets = UIEdgeInsetsMake(0, -(imageWith+2), 0, imageWith+2);
}

- (void)setLabelCenter {
    self.titleEdgeInsets = UIEdgeInsetsMake(0,0, 0, 0);
}

+ (void)load{
    Method newMethod = class_getInstanceMethod([self class], @selector(lzyLayoutSubviews));
    Method method = class_getInstanceMethod([self class], @selector(layoutSubviews));
    method_exchangeImplementations(newMethod, method);
}

- (void)setImageLayout:(UIButtonLayoutType)type space:(CGFloat)space{
    [self.infoDic setValue:@(type) forKey:@"type"];
    [self.infoDic setValue:@(space) forKey:@"space"];
    [self.infoDic setValue:@true forKey:@"support"];
    
    objc_setAssociatedObject(self, &infoDicKey, self.infoDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void)lzyLayoutSubviews{
    
    [self lzyLayoutSubviews];
    
    if (!self.support) {
        if (self.isSizeToFit) {
            [self.titleLabel sizeToFit];
        }
        return;
    }
    
    UIButtonLayoutType type = self.type;
    CGFloat space = self.space;
    
    CGSize titleSize = self.titleLabel.bounds.size;
    CGSize imageSize = self.imageView.frame.size;
    
    CGFloat fitOffset = 0;
    if (self.isSizeToFit) {
        [self.titleLabel sizeToFit];
        CGSize fitTitleSize = self.titleLabel.bounds.size;
        fitOffset = fitTitleSize.width - titleSize.width;
    }
    
    CGFloat spaceOffset = space/2.0;
    
    CGFloat imageWidthOffset = titleSize.width/2.0;
    CGFloat imageHeightOffset = titleSize.height/2.0;
    CGFloat titleWidthOffset = imageSize.width/2.0 + fitOffset/2.0;
    CGFloat titleHeightOffset = imageSize.height/2.0;
    
    switch (type) {
        case UIButtonLayoutImageTop:
            self.titleEdgeInsets = UIEdgeInsetsMake(titleHeightOffset+space, -titleWidthOffset, -titleHeightOffset-spaceOffset, +titleWidthOffset);
            self.imageEdgeInsets = UIEdgeInsetsMake(-imageHeightOffset-spaceOffset, imageWidthOffset, imageHeightOffset+spaceOffset, -imageWidthOffset);
            break;
        case UIButtonLayoutImageBottom:
            self.titleEdgeInsets = UIEdgeInsetsMake(-titleHeightOffset-spaceOffset, -titleWidthOffset, titleHeightOffset+spaceOffset, titleWidthOffset);
            self.imageEdgeInsets = UIEdgeInsetsMake(imageHeightOffset+spaceOffset, imageWidthOffset, -imageHeightOffset-spaceOffset, -imageWidthOffset);
            break;
        case UIButtonLayoutImageLeft:
            self.titleEdgeInsets = UIEdgeInsetsMake(0, spaceOffset, 0, -spaceOffset);
            self.imageEdgeInsets = UIEdgeInsetsMake(0, -spaceOffset, 0, spaceOffset);
            break;
        case UIButtonLayoutImageRight:
            self.titleEdgeInsets = UIEdgeInsetsMake(0, -2*titleWidthOffset - spaceOffset, 0, 2*titleWidthOffset + spaceOffset);
            self.imageEdgeInsets = UIEdgeInsetsMake(0, 2*imageWidthOffset + spaceOffset, 0, -2*imageWidthOffset - spaceOffset);
            break;
        default:
            break;
    }
}

- (void)setIsSizeToFit:(BOOL)isSizeToFit{
    [self.infoDic setValue:@(isSizeToFit) forKey:@"isSizeToFit"];
    objc_setAssociatedObject(self, &infoDicKey, self.infoDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)isSizeToFit{
    return [self.infoDic[@"isSizeToFit"] boolValue];
}

- (CGFloat)space{
    return [self.infoDic[@"space"] floatValue];
}

- (UIButtonLayoutType)type{
    return (UIButtonLayoutType)[self.infoDic[@"type"] integerValue];
}

- (BOOL)support{
    return [self.infoDic[@"support"] boolValue];
}

- (NSMutableDictionary *)infoDic{
    NSMutableDictionary *infoDic = objc_getAssociatedObject(self, &infoDicKey);
    if (!infoDic) {
        infoDic = [@{@"type":@(UIButtonLayoutImageLeft),@"space":@0,@"isSizeToFit":@false,@"support":@false} mutableCopy];
        objc_setAssociatedObject(self, &infoDicKey, infoDic, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    
    return infoDic;
}



@end

//
//  UIButton+ContentLayout.h
//  RMReader
//
//  Created by bx on 12/5/19.
//  Copyright © 2019 bx. All rights reserved.
//



#import <UIKit/UIKit.h>

typedef NS_ENUM(NSUInteger, UIButtonLayoutType) {
    UIButtonLayoutImageLeft,
    UIButtonLayoutImageRight,
    UIButtonLayoutImageTop,
    UIButtonLayoutImageBottom,
};


NS_ASSUME_NONNULL_BEGIN

@interface UIButton (ContentLayout)
/**
 titleLabel是否自适应宽度
 */
@property (assign, nonatomic) BOOL isSizeToFit;

- (void)setLabelLeftImageRight;
- (void)setLabelCenter;
- (void)setImageLayout:(UIButtonLayoutType)type space:(CGFloat)space;


@end

NS_ASSUME_NONNULL_END

//
//  UIButton+APExtension.swift
//  FuweiCloud
//
//  Created by michael on 2024/6/17.
//

import UIKit

extension UIControl.State: Hashable {}

@available(iOS 15.0, *)
extension UIButton {
    
    func config(titles: [UIControl.State: Any?]? = nil, images: [UIControl.State: Any?]? = nil) {
        var config = UIButton.Configuration.plain()
        let title = titles?[.normal]
        let image = images?[.normal]
        if title is String {
            config.attributedTitle = AttributedString(stringLiteral: title as! String)
        }else if title is NSAttributedString {
            config.attributedTitle = AttributedString(title as! NSAttributedString)
        }
        if image is UIImage {
            config.image = image as? UIImage
        }else if image is String {
            config.image = UIImage(named: image as! String)
        }
        
        config.imagePadding = 5
        config.imagePlacement = .top
        config.contentInsets = .zero
        var bgConfig = UIBackgroundConfiguration.clear()
        bgConfig.backgroundColor = .clear
        config.background = bgConfig
        configurationUpdateHandler = { btn in
            let title = titles?[btn.state]
            let image = images?[btn.state]
            if title is String {
                btn.configuration?.attributedTitle = AttributedString(stringLiteral: title as! String)
            }else if title is NSAttributedString {
                btn.configuration?.attributedTitle = AttributedString(title as! NSAttributedString)
            }
            if image is UIImage {
                btn.configuration?.image = image as? UIImage
            }else if image is String {
                btn.configuration?.image = UIImage(named: image as! String)
            }
        }
        configuration = config
    }
    
    convenience init(config titles: [UIControl.State: Any]? = nil, images: [UIControl.State: Any]? = nil) {
        self.init()
        config(titles: titles, images: images)
    }
}

//
//  UIButton+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/3/24.
//

#import "UIButton+APExtension.h"

@implementation UIButton (APExtension)

/*======================================== Title ===========================================*/
+ (instancetype)buttonWithTitle:(id)title {
    return [self buttonWithTitles:title images:nil bImages:nil states:nil];
}
+ (instancetype)buttonWithTitle:(id)title bImage:(nullable id)bImage {
    return [self buttonWithTitles:title images:nil bImages:bImage states:nil];
}
+ (instancetype)buttonWithTitles:(id)titles states:(NSArray *)states {
    return [self buttonWithTitles:titles images:nil bImages:nil states:states];
}
+ (instancetype)buttonWithTitles:(id)titles bImages:(nullable id)bImages states:(NSArray *)states {
    return [self buttonWithTitles:titles images:nil bImages:bImages states:states];
}
/*======================================== Title + Block ===========================================*/
+ (instancetype)buttonWithTitle:(id)title handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self buttonWithTitles:title images:nil bImages:nil states:nil];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)buttonWithTitle:(id)title bImage:(nullable id)bImage handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self buttonWithTitles:title images:nil bImages:bImage states:nil];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)buttonWithTitles:(id)titles states:(NSArray *)states handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self buttonWithTitles:titles images:nil bImages:nil states:states];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)buttonWithTitles:(id)titles bImages:(nullable id)bImages states:(NSArray *)states handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self buttonWithTitles:titles images:nil bImages:bImages states:states];
    [btn addEventHandler:handler];
    return btn;
}
/*======================================== Image ===========================================*/
+ (instancetype)buttonWithImage:(id)image {
    return [self buttonWithTitles:nil images:image bImages:nil states:nil];
}
+ (instancetype)buttonWithImage:(id)image bImage:(nullable id)bImage {
    return [self buttonWithTitles:nil images:image bImages:bImage states:nil];
}
+ (instancetype)buttonWithImages:(id)images states:(NSArray *)states {
    return [self buttonWithTitles:nil images:images bImages:nil states:states];
}
+ (instancetype)buttonWithImages:(id)images bImages:(nullable id)bImages states:(NSArray *)states {
    return [self buttonWithTitles:nil images:images bImages:bImages states:states];
}
/*======================================== Image + Block ===========================================*/
+ (instancetype)buttonWithImage:(id)image handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self buttonWithTitles:nil images:image bImages:nil states:nil];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)buttonWithImage:(id)image bImage:(nullable id)bImage handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self buttonWithTitles:nil images:image bImages:bImage states:nil];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)buttonWithImages:(id)images states:(NSArray *)states handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self buttonWithTitles:nil images:images bImages:nil states:states];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)buttonWithImages:(id)images bImages:(nullable id)bImages states:(NSArray *)states handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self buttonWithTitles:nil images:images bImages:bImages states:states];
    [btn addEventHandler:handler];
    return btn;
}
/*======================================== Mixed ===========================================*/
+ (instancetype)buttonWithTitle:(id)title image:(id)image {
    return [self buttonWithTitles:title images:image bImages:nil states:nil];
}
+ (instancetype)buttonWithTitle:(id)title image:(id)image bImage:(nullable id)bImage {
    return [self buttonWithTitles:title images:image bImages:bImage states:nil];
}
+ (instancetype)buttonWithTitles:(id)titles images:(id)images states:(NSArray *)states {
    return [self buttonWithTitles:titles images:images bImages:nil states:states];
}
+ (instancetype)buttonWithTitles:(id)titles images:(id)images bImages:(id)bImages states:(NSArray *)states {
    UIButton *btn = [self buttonWithType:UIButtonTypeCustom];
    [btn setTitles:titles images:images bImages:bImages states:states];
    return btn;
}
/*======================================== Mixed + Block ===========================================*/
+ (instancetype)buttonWithTitle:(id)title image:(id)image handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self buttonWithTitles:title images:image bImages:nil states:nil];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)buttonWithTitle:(id)title image:(id)image bImage:(nullable id)bImage handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self buttonWithTitles:title images:image bImages:bImage states:nil];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)buttonWithTitles:(id)titles images:(id)images states:(NSArray *)states handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self buttonWithTitles:titles images:images bImages:nil states:states];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)buttonWithTitles:(id)titles images:(id)images bImages:(id)bImages states:(NSArray *)states handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self buttonWithTitles:titles images:images bImages:bImages states:states];
    [btn addEventHandler:handler];
    return btn;
}


- (void)setTitles:(id)titles images:(id)images bImages:(id)bImages states:(NSArray *)states {
    states = states?:@[@0];
    [states enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        UIControlState state = [obj unsignedIntegerValue];
        /// aString
        [self setAttributes:titles idx:idx state:state type:0];
        /// image
        [self setAttributes:images idx:idx state:state type:1];
        /// background image
        [self setAttributes:bImages idx:idx state:state type:2];
    }];
}

- (void)setAttributes:(id)attributes states:(NSArray *)states type:(NSInteger)type {
    states = states?:@[@0];
    [states enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        UIControlState state = [obj unsignedIntegerValue];
        [self setAttributes:attributes idx:idx state:state type:type];
    }];
}

- (void)setImage:(id)image {
    [self setAttributes:image states:nil type:1];
}

- (void)setImage:(id)image state:(NSUInteger)state {
    [self setAttributes:image states:@[@(state)] type:1];
}

- (void)setImages:(id)images states:(NSArray *)states {
    [self setAttributes:images states:states type:1];
}

- (void)setTitler:(id)title {
    [self setAttributes:title states:nil type:0];
}

- (void)setTitle:(id)title state:(NSUInteger)state {
    [self setAttributes:title states:@[@(state)] type:0];
}

- (void)setTitles:(id)titles states:(NSArray *)states {
    [self setAttributes:titles states:states type:0];
}

- (void)setBImage:(id)bImage {
    [self setAttributes:bImage states:nil type:2];
}

- (void)setBImage:(id)bImage state:(NSUInteger)state {
    [self setAttributes:bImage states:@[@(state)] type:2];
}

- (void)setBImages:(id)bImages states:(NSArray *)states {
    [self setAttributes:bImages states:states type:2];
}

- (void)setTitle:(id)title image:(id)image {
    [self setAttributes:title states:nil type:0];
    [self setAttributes:image states:nil type:1];
}

- (void)setTitle:(id)title image:(id)image state:(NSUInteger)state {
    [self setAttributes:title states:@[@(state)] type:0];
    [self setAttributes:image states:@[@(state)] type:1];
}

- (void)setTitles:(id)titles images:(id)images states:(NSArray *)states {
    [self setAttributes:titles states:states type:0];
    [self setAttributes:images states:states type:1];
}


- (void)setAttribute:(id)attribute state:(NSUInteger)state type:(NSInteger)type {
    if (type == 0) { ///< string
        if (APISStr(attribute)||APISAStr(attribute)) {
            [self setAttributedTitle:AString(attribute, @17.6, [UIColor labelColor]) forState:state];
        }else {
            [self setAttributedTitle:nil forState:state];
        }
    } else if (type == 1) {///< 图标
        UIImage *image = nil;
        if (APISStr(attribute)) {
            image = [APImageNamed(attribute) imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
        
        } else if ([attribute isKindOfClass:[UIImage class]]) {
            image = [attribute imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal];
        }
        [self setImage:image forState:state];
    } else { ///< 背景图
        if (APISStr(attribute)) {
            [self setBackgroundImage:APImageNamed(attribute) forState:state];
        } else if ([attribute isKindOfClass:[UIColor class]]) {
            [self setBackgroundImage:[UIImage imageWithColor:attribute] forState:state];
        } else if ([attribute isKindOfClass:[UIImage class]]) {
            [self setBackgroundImage:attribute forState:state];
        } else {
            [self setBackgroundImage:nil forState:state];
        }
    }
}

- (void)setAttributes:(id)attributes idx:(NSUInteger)idx state:(NSUInteger)state type:(NSInteger)type {
    if ([attributes isKindOfClass:[NSArray class]]) {
        if (idx < [attributes count]) {
            id attr = attributes[idx];
            [self setAttribute:attr state:state type:type];
        }
    }else {
        [self setAttribute:attributes state:state type:type];
    }
}


+ (UIButton *)apCornerButtonWithTitle:(NSString *)title normal:(BOOL)isNormal target:(nullable id)target selector:(nullable SEL)sel {
    UIColor *bColor = APButtonColor, *wColor = [UIColor whiteColor];
    NSArray *colors1 = @[bColor, wColor], *colors2 = @[wColor, bColor];
    let btn = [self buttonWithTitles:AStrings(title, @17.6, isNormal?colors1:colors2) bImages:isNormal?colors2:colors1 states:@[@0,@1]];
    [btn addTarget:target action:sel forControlEvents:UIControlEventTouchUpInside];
    btn.clipsToBounds = YES;
    btn.layer.cornerRadius = 20;
    btn.layer.borderColor = bColor.CGColor;
    btn.layer.borderWidth = 1.2;
    return btn;
}

- (void)setApEnabled:(BOOL)enabled {
    self.enabled = enabled;
    self.layer.borderWidth = enabled?1:0;
}

- (BOOL)apEnabled {
    return self.enabled;
}

+ (instancetype)apCornerButton:(id)title action:(SEL)action target:(id)target normal:(BOOL)normal height:(CGFloat)height {
    UIColor *bColor = APButtonColor, *wColor = [UIColor whiteColor];
    NSArray *colors1 = @[bColor, wColor], *colors2 = @[wColor, bColor];
    let btn = [self buttonWithTitles:AStrings(title, @17.6, normal?colors1:colors2) bImages:normal?colors2:colors1 states:@[@0,@1]];
    if (action && target) [btn addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
    CGFloat r = 20;
    if (FW_LNDU_FLAG) r = height / 2;
    [btn addCorner:r borderWidth:1 borderColor:bColor];
    return btn;
}

+ (instancetype)apCornerButton:(id)title normal:(BOOL)normal {
    return [self apCornerButton:title action:NULL target:nil normal:normal height:40];
}

+ (instancetype)apCornerButton:(id)title normal:(BOOL)normal height:(CGFloat)height {
    return [self apCornerButton:title action:NULL target:nil normal:normal height:height];
}

+ (instancetype)apCornerButton:(id)title action:(SEL)action target:(id)target normal:(BOOL)normal {
    return [self apCornerButton:title action:action target:target normal:normal height:40];
}
+ (instancetype)apCornerButton:(id)title normal:(BOOL)normal handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self apCornerButton:title action:NULL target:nil normal:normal height:40];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)apCornerButton:(id)title normal:(BOOL)normal height:(CGFloat)height handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self apCornerButton:title action:NULL target:nil normal:normal height:height];
    [btn addEventHandler:handler];
    return btn;
}

+ (instancetype)apColorButton:(id)title action:(SEL)action target:(id)target bImage:(id)bImage height:(CGFloat)height {
    let btn = [self buttonWithTitle:title bImage:bImage];
    if (action && target) [btn addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
    CGFloat r = 20;
    if (FW_LNDU_FLAG) r = height / 2;
    [btn addCorner:r];
    return btn;
}

+ (instancetype)apColorButton:(id)title bImage:(id)bImage {
    return [self apColorButton:title action:NULL target:nil bImage:bImage height:55];
}

+ (instancetype)apColorButton:(id)title bImage:(id)bImage height:(CGFloat)height {
    return [self apColorButton:title action:NULL target:nil bImage:bImage height:height];
}

+ (instancetype)apColorButton:(id)title action:(SEL)action target:(id)target bImage:(id)bImage {
    return [self apColorButton:title action:action target:target bImage:bImage height:55];
}
+ (instancetype)apColorButton:(id)title bImage:(id)bImage handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self apColorButton:title action:NULL target:nil bImage:bImage height:55];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)apColorButton:(id)title bImage:(id)bImage height:(CGFloat)height handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self apColorButton:title action:NULL target:nil bImage:bImage height:height];
    [btn addEventHandler:handler];
    return btn;
}



+ (instancetype)apBorderButton:(id)title action:(SEL)action target:(id)target height:(CGFloat)height {
    let btn = [self buttonWithTitle:AString(title, @17.6, [UIColor labelColor]) bImage:nil];
    if (action && target) [btn addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
    CGFloat r = 16;
    if (FW_LNDU_FLAG) r = height / 2;
    [btn addCorner:r borderWidth:1 borderColor:APGray1(d)];
    return btn;
}

+ (instancetype)apBorderButton:(id)title {
    return [self apBorderButton:title action:NULL target:nil height:40];
}

+ (instancetype)apBorderButton:(id)title height:(CGFloat)height {
    return [self apBorderButton:title action:NULL target:nil height:height];
}

+ (instancetype)apBorderButton:(id)title action:(SEL)action target:(id)target {
    return [self apBorderButton:title action:action target:target height:40];
}
+ (instancetype)apBorderButton:(id)title handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self apBorderButton:title action:NULL target:nil height:40];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)apBorderButton:(id)title height:(CGFloat)height handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self apBorderButton:title action:NULL target:nil height:height];
    [btn addEventHandler:handler];
    return btn;
}



+ (instancetype)apGradientButton:(id)title action:(nullable SEL)action target:(nullable id)target height:(CGFloat)height {
    let btn = [self buttonWithTitle:AString(title, @17.6, FWColors(APGray1(3), APWhiteColor, APWhiteColor)) bImage:[UIImage theme1Gradient]];
    [btn.titleLabel scale:0.8 lines:2];
    if (action && target) [btn addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
    if (FW_LNDU_FLAG) [btn addCorner:height/2];
    else [btn addCorner:16];
    return btn;
}

+ (instancetype)apGradientButton:(id)title {
    return [self apGradientButton:title action:NULL target:nil height:55];
}

+ (instancetype)apGradientButton:(id)title height:(CGFloat)height {
    return [self apGradientButton:title action:NULL target:nil height:height];
}

+ (instancetype)apGradientButton:(id)title action:(SEL)action target:(id)target {
    return [self apGradientButton:title action:action target:target height:55];
}
+ (instancetype)apGradientButton:(id)title handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self apGradientButton:title action:NULL target:nil height:55];
    [btn addEventHandler:handler];
    return btn;
}
+ (instancetype)apGradientButton:(id)title height:(CGFloat)height handler:(nonnull APButtonTouchInsideHandler)handler {
    UIButton *btn = [self apGradientButton:title action:NULL target:nil height:height];
    [btn addEventHandler:handler];
    return btn;
}



- (void)setApLoading:(BOOL)apLoading {
    [self setAttributedTitle:self.currentAttributedTitle forState:UIControlStateNormal];
    if (apLoading) {
        [self setImage:APImageNamed(@"btn_loading") forState:UIControlStateNormal];
        [self kj_contentLayout:KJButtonContentLayoutStyleNormal padding:5];
        [self.imageView rotateImage:YES];
    }else {
        [self setImage:nil forState:UIControlStateNormal];
        [self.imageView rotateImage:NO];
    }
}

- (BOOL)apLoading {
    return [self.imageView.layer animationKeys].count > 0;
}

- (void)addAction:(SEL)action onTarget:(id)target {
    if (action && target) {
        [self addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
    }
}

- (void)addEventHandler:(void (^)(UIButton *btn))handler {
    [self addEventHandler:handler forControlEvents:UIControlEventTouchUpInside];
}

- (void)setTouchAreaInsets:(UIEdgeInsets)touchAreaInsets{
    NSValue *value = [NSValue valueWithUIEdgeInsets:touchAreaInsets];
    objc_setAssociatedObject(self, @selector(touchAreaInsets), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

//
//  UIButton+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/3/24.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

#define APButtonLoading(x) if ((x).apLoading) return; (x).apLoading = YES;
#define APButtonLoading1(x,y) if ((x).apLoading) return y; (x).apLoading = YES;

typedef void(^APButtonTouchInsideHandler)(UIButton *btn);
@interface UIButton (APExtension)

/// image
+ (instancetype)buttonWithImage:(nullable id)image;
+ (instancetype)buttonWithImages:(nullable id)images states:(nullable NSArray *)states;
+ (instancetype)buttonWithImage:(nullable id)image bImage:(nullable id)bImage;
+ (instancetype)buttonWithImages:(nullable id)images bImages:(nullable id)bImages states:(nullable NSArray *)states;
/// image + block
+ (instancetype)buttonWithImage:(nullable id)image handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)buttonWithImages:(nullable id)images states:(nullable NSArray *)states handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)buttonWithImage:(nullable id)image bImage:(nullable id)bImage handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)buttonWithImages:(nullable id)images bImages:(nullable id)bImages states:(nullable NSArray *)states handler:(APButtonTouchInsideHandler)handler;

///< title
+ (instancetype)buttonWithTitle:(nullable id)title;
+ (instancetype)buttonWithTitle:(nullable id)title bImage:(nullable id)bImage;
+ (instancetype)buttonWithTitles:(nullable id)titles states:(nullable NSArray *)states;
+ (instancetype)buttonWithTitles:(nullable id)titles bImages:(nullable id)bImages states:(nullable NSArray *)states;
///< title + block
+ (instancetype)buttonWithTitle:(nullable id)title handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)buttonWithTitle:(nullable id)title bImage:(nullable id)bImage handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)buttonWithTitles:(nullable id)titles states:(nullable NSArray *)states handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)buttonWithTitles:(nullable id)titles bImages:(nullable id)bImages states:(nullable NSArray *)states handler:(APButtonTouchInsideHandler)handler;

///< mixed
+ (instancetype)buttonWithTitle:(nullable id)aString image:(nullable id)image;
+ (instancetype)buttonWithTitles:(nullable id)aStrings images:(nullable id)images states:(nullable NSArray *)states;
+ (instancetype)buttonWithTitle:(nullable id)aString image:(nullable id)image bImage:(nullable id)bImage;
+ (instancetype)buttonWithTitles:(nullable id)aStrings images:(nullable id)images bImages:(nullable id)bImages states:(nullable NSArray *)states;
///< mixed + block
+ (instancetype)buttonWithTitle:(nullable id)aString image:(nullable id)image handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)buttonWithTitles:(nullable id)aStrings images:(nullable id)images states:(nullable NSArray *)states handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)buttonWithTitle:(nullable id)aString image:(nullable id)image bImage:(nullable id)bImage handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)buttonWithTitles:(nullable id)aStrings images:(nullable id)images bImages:(nullable id)bImages states:(nullable NSArray *)states handler:(APButtonTouchInsideHandler)handler;

///< setter
- (void)setImage:(nullable id)image;
- (void)setImage:(nullable id)image state:(NSUInteger)state;
- (void)setImages:(nullable id)images states:(nullable NSArray *)states;

- (void)setTitler:(nullable id)title;
- (void)setTitle:(nullable id)title state:(NSUInteger)state;
- (void)setTitles:(nullable id)titles states:(nullable NSArray *)states;

- (void)setBImage:(nullable id)bImage;
- (void)setBImage:(nullable id)bImage state:(NSUInteger)state;
- (void)setBImages:(nullable id)bImages states:(nullable NSArray *)states;

- (void)setTitle:(nullable id)title image:(nullable id)image;
- (void)setTitle:(nullable id)title image:(nullable id)image state:(NSUInteger)state;

- (void)setTitles:(nullable id)titles images:(nullable id)images states:(nullable NSArray *)states;
- (void)setTitles:(nullable id)titles images:(nullable id)images bImages:(nullable id)bImages states:(nullable NSArray *)states;



@property (nonatomic, assign) BOOL apEnabled;

+ (instancetype)apCornerButton:(id)title normal:(BOOL)normal;
+ (instancetype)apCornerButton:(id)title normal:(BOOL)normal height:(CGFloat)height;
+ (instancetype)apCornerButton:(id)title action:(nullable SEL)action target:(nullable id)target normal:(BOOL)normal;
+ (instancetype)apCornerButton:(id)title action:(nullable SEL)action target:(nullable id)target normal:(BOOL)normal height:(CGFloat)height;
+ (instancetype)apCornerButton:(id)title normal:(BOOL)normal handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)apCornerButton:(id)title normal:(BOOL)normal height:(CGFloat)height handler:(APButtonTouchInsideHandler)handler;

+ (instancetype)apColorButton:(id)title bImage:(id)bImage;
+ (instancetype)apColorButton:(id)title bImage:(id)bImage height:(CGFloat)height;
+ (instancetype)apColorButton:(id)title action:(nullable SEL)action target:(nullable id)target bImage:(id)bImage;
+ (instancetype)apColorButton:(id)title action:(nullable SEL)action target:(nullable id)target bImage:(id)bImage height:(CGFloat)height;
+ (instancetype)apColorButton:(id)title bImage:(id)bImage handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)apColorButton:(id)title bImage:(id)bImage height:(CGFloat)height handler:(APButtonTouchInsideHandler)handler;

+ (instancetype)apBorderButton:(id)title;
+ (instancetype)apBorderButton:(id)title height:(CGFloat)height;
+ (instancetype)apBorderButton:(id)title action:(nullable SEL)action target:(nullable id)target;
+ (instancetype)apBorderButton:(id)title action:(nullable SEL)action target:(nullable id)target height:(CGFloat)height;
+ (instancetype)apBorderButton:(id)title handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)apBorderButton:(id)title height:(CGFloat)height handler:(APButtonTouchInsideHandler)handler;

+ (instancetype)apGradientButton:(id)title;
+ (instancetype)apGradientButton:(id)title height:(CGFloat)height;
+ (instancetype)apGradientButton:(id)title action:(nullable SEL)action target:(nullable id)target height:(CGFloat)height;
+ (instancetype)apGradientButton:(id)title action:(nullable SEL)action target:(nullable id)target;
+ (instancetype)apGradientButton:(id)title handler:(APButtonTouchInsideHandler)handler;
+ (instancetype)apGradientButton:(id)title height:(CGFloat)height handler:(APButtonTouchInsideHandler)handler;

@property (nonatomic, assign) BOOL apLoading;

- (void)addAction:(nullable SEL)action onTarget:(nullable id)target;
- (void)addEventHandler:(APButtonTouchInsideHandler)handler;

/// 设置按钮额外热区
@property (nonatomic, assign) UIEdgeInsets touchAreaInsets;

@end

NS_ASSUME_NONNULL_END

//
//  UIAlertController+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/5/8.
//

#import "UIAlertController+APExtension.h"

@implementation UIAlertController (APExtension)

+ (instancetype)confirmAlert:(NSString *)title confirmBlock:(void (^)(void))confirmBlock {
    return [self confirmAlert:title message:nil confirmTitle:@"Confirm".local confirmBlock:confirmBlock cancelBlock:nil];
}

+ (instancetype)confirmAlert:(NSString *)title message:(NSString *)message confirmBlock:(void (^)(void))confirmBlock {
    return [self confirmAlert:title message:message confirmTitle:@"Confirm".local confirmBlock:confirmBlock cancelBlock:nil];
}

+ (instancetype)confirmAlert:(NSString *)title message:(NSString *)message confirmBlock:(void (^)(void))confirmBlock cancelBlock:(void (^)(void))cancelBlock {
    return [self confirmAlert:title message:message confirmTitle:@"Confirm".local confirmBlock:confirmBlock cancelBlock:cancelBlock];
}

+ (instancetype)confirmAlert:(NSString *)title message:(NSString *)message confirmTitle:(NSString *)confirmTitle confirmBlock:(void (^)(void))confirmBlock cancelBlock:(void (^)(void))cancelBlock {
    let vc = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
    let cancelAction = [UIAlertAction actionWithTitle:@"Cancel".local style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        AP_SAFE_BLOCK(cancelBlock);
    }];
    UIAlertActionStyle style = [confirmTitle compare:@"Delete".local] ? 0 : 2;
    let confirmAction = [UIAlertAction actionWithTitle:confirmTitle style:style handler:^(UIAlertAction * _Nonnull action) {
        AP_SAFE_BLOCK(confirmBlock);
    }];
    [vc addAction:cancelAction];
    
    [vc addAction:confirmAction];
    [APKeyWindow.rootViewController presentVC:vc];
    return vc;
}

+ (instancetype)forceAlert:(NSString *)title message:(NSString *)message confirmTitle:(NSString *)confirmTitle confirmBlock:(void (^)(void))confirmBlock {
    let vc = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert];
    [vc addAction:[UIAlertAction actionWithTitle:confirmTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        AP_SAFE_BLOCK(confirmBlock);
    }]];
    [APKeyWindow.rootViewController presentVC:vc];
    return vc;
}

+ (instancetype)forceAlert:(NSString *)title message:(NSString *)message confirmBlock:(void (^)(void))confirmBlock {
    return [self forceAlert:title message:message confirmTitle:@"Confirm".local confirmBlock:confirmBlock];
}

@end

//
//  UIAlertController+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/5/8.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIAlertController (APExtension)


+ (instancetype)confirmAlert:(nullable NSString *)title confirmBlock:(void(^_Nullable)(void))confirmBlock;
+ (instancetype)confirmAlert:(nullable NSString *)title message:(nullable NSString *)message confirmBlock:(void(^_Nullable)(void))confirmBlock;
+ (instancetype)confirmAlert:(nullable NSString *)title message:(nullable NSString *)message confirmBlock:(void(^_Nullable)(void))confirmBlock cancelBlock:(void(^_Nullable)(void))cancelBlock;
+ (instancetype)confirmAlert:(nullable NSString *)title message:(nullable NSString *)message confirmTitle:(nullable NSString *)confirmTitle confirmBlock:(void(^_Nullable)(void))confirmBlock cancelBlock:(void(^_Nullable)(void))cancelBlock;

+ (instancetype)forceAlert:(nullable NSString *)title message:(nullable NSString *)message confirmTitle:(NSString *)confirmTitle confirmBlock:(void(^_Nullable)(void))confirmBlock;
+ (instancetype)forceAlert:(nullable NSString *)title message:(nullable NSString *)message confirmBlock:(void(^_Nullable)(void))confirmBlock;

@end

NS_ASSUME_NONNULL_END

//
//  String+APExtension.swift
//  FuweiCloud
//
//  Created by michael on 2024/5/14.
//

import Foundation

// MARK: -  String分类  工具

extension String {
    var local: String {
        NSLocalizedString(self, comment: self)
    }
    
    /// 字符串的宽
    func width(_ font: Any) -> CGFloat {
        self.width(for: UIFont.idFont(font))
    }
    /// 字符串的高
    func height(_ font: Any, _ width: CGFloat) -> CGFloat {
        self.height(for: UIFont.idFont(font), width: width)
    }
    
    var fileURL: URL { URL(fileURLWithPath: self) }
    
    /// 空字符串判断
    var isBlank:Bool{ allSatisfy{ $0.isWhitespace} }
    
    subscript(r: Range<Int>) -> String {
        get {
            return String(self[startIndex..<endIndex])
        }
    }
    
    /// 字符串截断
    func subStrToIndex(index: Int) -> String {
        return (self as NSString).substring(to: index)
    }
}

extension Substring {
    var string: String { String(self) }
}

extension String {
    
    /// HTML标签 转 html 富文本  font: 设置字体 lineSpacing: 设置行高 color: 设置颜色
    func setHtmlAttributedString(font: UIFont? = UIFont.systemFont(ofSize: 16), lineSpacing: CGFloat? = 10, color:UIColor? = nil) -> NSMutableAttributedString {
        var htmlString: NSMutableAttributedString? = nil
        do {
            if let data = self.data(using: .utf8) { // .unicode
                htmlString = try NSMutableAttributedString(data: data, options: [
                    NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.html,
                    NSAttributedString.DocumentReadingOptionKey.characterEncoding: NSNumber(value: String.Encoding.utf8.rawValue)], documentAttributes: nil)
            }
        } catch {
        }
        if let font = font { // 设置富文本字体
            htmlString?.addAttributes([
            NSAttributedString.Key.font: font], range: NSRange(location: 0, length: htmlString?.length ?? 0))
        }
        if let weakLineSpacing = lineSpacing { // 设置行间距
            let paragraphStyle = NSMutableParagraphStyle()
            paragraphStyle.lineSpacing = weakLineSpacing
            htmlString?.addAttribute(.paragraphStyle, value: paragraphStyle, range: NSRange(location: 0, length: htmlString?.length ?? 0))
        }
        if let color = color { // 设置颜色
            htmlString?.addAttributes([
            NSAttributedString.Key.foregroundColor: color], range: NSRange(location: 0, length: htmlString?.length ?? 0))
        }
        return htmlString ?? NSMutableAttributedString(string: self)
    }
    
    
    /// 两个时间字符串比较大小  startTime: 开始时间 endTime: 结束时间 dateFormat: 时间格式
    func compareTime(_ startTime:String? = nil, _  endTime:String? = nil, _ dateFormat:String = "yyyy-MM-dd HH:mm:ss") ->  ComparisonResult {
        guard let startTime,let endTime else { return .orderedSame }
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = dateFormat
        let start = dateFormatter.date(from: startTime)
        let startStamp = start!.timeIntervalSince1970*1000
        let end = dateFormatter.date(from: endTime)
        let endStamp = end!.timeIntervalSince1970*1000
        
        if startStamp < endStamp { // 当前时间早于指定时间
            return .orderedAscending
        } else if startStamp > endStamp { // 当前时间晚于指定时间
            return .orderedDescending
        } else { // 当前时间与指定时间相同
            return .orderedSame
        }
    }
}

//
//  Number+APExtension.swift
//  FuweiCloud
//
//  Created by michael on 2024/4/29.
//

import Foundation
import CoreGraphics

extension CGFloat {
    var size: CGSize {
        if self <= 0 {
            .zero
        } else {
            CGSizeMake(self, self)
        }
    }
    var rect: CGRect {
        if self <= 0 {
            .zero
        } else {
            CGRectMake(0, 0, self, self)
        }
    }
    var point: CGPoint {
        CGPointMake(self, self)
    }
    var inset: UIEdgeInsets {
        UIEdgeInsets(top: self, left: self, bottom: self, right: self)
    }
}

extension BinaryInteger {
    var int8Data: Data { Data.int8(Int8(self)) }
    var uint8Data: Data { Data.uint8(UInt8(self)) }
    var int16Data: Data { Data.int16(Int16(self)) }
    var uint16Data: Data { Data.uint16(UInt16(self)) }
    var int32Data: Data { Data.int32(Int32(self)) }
    var uint32Data: Data { Data.uint32(UInt32(self)) }
    var int64Data: Data { Data.int64(Int64(self)) }
    var uint64Data: Data { Data.uint64(UInt64(self)) }
}


//
//  NSString+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/7/7.
//

#import "NSString+APExtension.h"

@implementation NSString (APExtension)

- (NSData *)data {
    return [self dataUsingEncoding:NSUTF8StringEncoding];
}

- (NSString *)mac {
    return [self stringByReplacingOccurrencesOfString:@"arlf" withString:@""].lowercaseString;
}

- (NSString *)macFormat {
    NSString *macStr = @"";
    for (int i = 0; i < self.length; i+=2) {
        macStr = [macStr stringByAppendingFormat:@"%@%@",[self substringWithRange:NSMakeRange(i, 2)].uppercaseString,(i==(self.length-2))?@"":@":"];
    }
    return macStr;
}

- (NSString *)macUnformat {
    return [self stringByReplacingOccurrencesOfString:@":" withString:@""].lowercaseString;
}

- (NSString *)uncapitalizedString {
    if (self.length) {
        return [self stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[self substringToIndex:1] lowercaseString]];
    }else {
        return [self lowercaseString];
    }
}

- (NSString *)alertSymbol {
    return [@"* " stringByAppendingString:self];
}

- (NSString *)urlStr {
    static NSString * const kAFCharactersGeneralDelimitersToEncode = @":#[]@"; // does not include "?" or "/" due to RFC 3986 - Section 3.4
    static NSString * const kAFCharactersSubDelimitersToEncode = @"!$&'()*+,;=";

    NSMutableCharacterSet * allowedCharacterSet = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
    [allowedCharacterSet removeCharactersInString:[kAFCharactersGeneralDelimitersToEncode stringByAppendingString:kAFCharactersSubDelimitersToEncode]];

    // FIXME: https://github.com/AFNetworking/AFNetworking/pull/3028
    // return [string stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];

    static NSUInteger const batchSize = 50;

    NSUInteger index = 0;
    NSMutableString *escaped = @"".mutableCopy;

    while (index < self.length) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wgnu"
        NSUInteger length = MIN(self.length - index, batchSize);
#pragma GCC diagnostic pop
        NSRange range = NSMakeRange(index, length);

        // To avoid breaking up character sequences such as 👴🏻👮🏽
        range = [self rangeOfComposedCharacterSequencesForRange:range];

        NSString *substring = [self substringWithRange:range];
        NSString *encoded = [substring stringByAddingPercentEncodingWithAllowedCharacters:allowedCharacterSet];
        [escaped appendString:encoded];

        index += range.length;
    }

    return escaped;
}

- (NSString *)videoLocalPath {
    return APString(@"%@/%@.%@",[APTools videoCachePath],[self md5String],[self pathExtension]);
}

- (NSString *)local {
    return NSLocalizedString(self, @"");
}

- (NSString *)local64 {
    return [NSString stringWithBase64EncodedString:NSLocalizedString(self, @"")];
}

+ (NSString *)makeNonce:(NSUInteger)length {
    char nonceStr[length+1];
    char count = 62;
    for (int i = 0; i < length; i++) {
        char nonce = rand() % count;
        if (nonce < 10) nonce += 48; ///< 数字
        else if (nonce < 36) nonce += 55; ///< 大写
        else nonce += 61; ///< 小写
        nonceStr[i] = nonce;
    }
    nonceStr[length] = '\0';
    return [NSString stringWithUTF8String:nonceStr];
}

- (NSString *)validateRegEx:(NSString *)regEx {
    if ([self isMatch:RX(regEx)]) return nil;
    if (![regEx compare:APLoginPwdRegEx]) return @"* The password must be 8-12 characters or numbers and case sensitive".local;
    if (![regEx compare:APcnPhoneRegEx]) return @"* Your mobile number is in the wrong format, please re-enter".local;
    if (![regEx compare:APtwPhoneRegEx]) return @"* Your mobile number is in the wrong format, please re-enter".local;
    if (![regEx compare:APEmailRegEx]) return @"* Your email is in the wrong format, please re-enter".local;
    if (![regEx compare:APDevicePwdRegEx]) return @"Please enter a 6-8 digit pure numeric password".local;
    if (![regEx compare:APEventNoteRegEx]) return @"Please enter a note name within 1-20 words".local;
    if (![regEx compare:APCaptchaRegEx]) return @"validate code error".local;
    return nil;
}

@end

//
//  NSString+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/7/7.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

#define APValidRegEx(x, y, c) NSString *_msg_ = [x validateRegEx:y];\
if (_msg_) { c; return; }\

@interface NSString (APExtension)

- (NSString *)uncapitalizedString;


@property (nonatomic, copy, readonly) NSString *alertSymbol;
@property (nonatomic, copy, readonly) NSString *local;
@property (nonatomic, copy, readonly) NSString *local64;

@property (nonatomic, copy, readonly) NSString *videoLocalPath;

@property (nonatomic, copy, readonly) NSString *urlStr;

@property (nonatomic, copy, readonly) NSData *data;

/// 去掉arlf部分
@property (nonatomic, copy, readonly) NSString *mac;
///< 转化为:分割的格式
@property (nonatomic, copy, readonly) NSString *macFormat;
///< 去掉:分割的格式
@property (nonatomic, copy, readonly) NSString *macUnformat;

+ (NSString *)makeNonce:(NSUInteger)length;

- (nullable NSString *)validateRegEx:(NSString *)regEx;



@end

NS_ASSUME_NONNULL_END

//
//  NSData+APExtension.h
//  ArphaPlus
//
//  Created by michael on 2024/4/18.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN


#define Int8Data(x) [NSData dataWithInt8:x]
#define UInt8Data(x) [NSData dataWithUInt8:x]
#define Int16Data(x) [NSData dataWithInt16:x]
#define UInt16Data(x) [NSData dataWithUInt16:x]
#define Int32Data(x) [NSData dataWithInt32:x]
#define UInt32Data(x) [NSData dataWithUInt32:x]
#define Int64Data(x) [NSData dataWithInt64:x]
#define UInt64Data(x) [NSData dataWithUInt64:x]
#define StrData(x) [NSData dataWithUTF8String:x]
#define EmptyData(a) [NSData dataWithLength:a]
#define ArrData2(a,b) [NSData dataWithArray:@[a,b]]
#define ArrData3(a,b,c) [NSData dataWithArray:@[a,b,c]]
#define ArrData4(a,b,c,d) [NSData dataWithArray:@[a,b,c,d]]
#define DataFrom(d,f) [d subdataWithRange:NSMakeRange(f, d.length-f)]
#define DataTo(d,t) [d subdataWithRange:NSMakeRange(0, MIN(t,d.length))]
#define DataFromTo(d,f,t) [d subdataWithRange:NSMakeRange(f, t-f)]
#define DataFromLen(d,f,l) [d subdataWithRange:NSMakeRange(f, l)]
#define RemoveData(d,l) [d removeData:l]

@interface NSData(APExtension)

+ (instancetype)dataWithInt8:(int8_t)integer;
+ (instancetype)dataWithUInt8:(uint8_t)integer;
+ (instancetype)dataWithInt16:(int16_t)integer;
+ (instancetype)dataWithUInt16:(uint16_t)integer;
+ (instancetype)dataWithInt32:(int32_t)integer;
+ (instancetype)dataWithUInt32:(uint32_t)integer;
+ (instancetype)dataWithInt64:(int64_t)integer;
+ (instancetype)dataWithUInt64:(uint64_t)integer;
+ (instancetype)dataWithUTF8String:(NSString *)string;
+ (instancetype)dataWithArray:(NSArray<NSData *> *)datas;
+ (instancetype)dataWithFormat:(NSData *)format, ...;
+ (instancetype)dataWithHEX:(NSString *)hex;
+ (instancetype)dataWithLength:(NSUInteger)length;

@property (nonatomic, assign, readonly) int8_t int8;
@property (nonatomic, assign, readonly) uint8_t uint8;
@property (nonatomic, assign, readonly) int16_t int16;
@property (nonatomic, assign, readonly) uint16_t uint16;
@property (nonatomic, assign, readonly) int32_t int32;
@property (nonatomic, assign, readonly) uint32_t uint32;
@property (nonatomic, assign, readonly) int64_t int64;
@property (nonatomic, assign, readonly) uint64_t uint64;
@property (nonatomic, copy, readonly) NSString *hex;
@property (nonatomic, copy, readonly) NSString *utf8;

- (int8_t)int8At:(NSUInteger)idx;
- (uint8_t)uint8At:(NSUInteger)idx;
- (int16_t)int16At:(NSUInteger)idx;
- (uint16_t)uint16At:(NSUInteger)idx;
- (int32_t)int32At:(NSUInteger)idx;
- (uint32_t)uint32At:(NSUInteger)idx;
- (int64_t)int64At:(NSUInteger)idx;
- (uint64_t)uint64At:(NSUInteger)idx;

- (NSData *)aes128EncryptWithKey:(NSData *)key;
- (NSData *)aes128DecryptWithKey:(NSData *)key;

@end

@interface NSMutableData (APExtension)

- (NSData *)removeData:(NSUInteger)length;

@end

NS_ASSUME_NONNULL_END

//
//  NSData+APExtension.m
//  ArphaPlus
//
//  Created by michael on 2024/4/18.
//

#import "NSData+APExtension.h"
#import <CommonCrypto/CommonCryptor.h>

#define DATA_INT_IMP return [NSData dataWithBytes:&integer length:sizeof(integer)];

#define INT_DATA_IMP(type) - (type##_t)type {                 \
type##_t num = 0;                                              \
[self getBytes:&num length:MIN(sizeof(num),self.length)];      \
return num;                                                    \
}                                                              \

#define INT_DATA_IMP1(type) - (type##_t)type##At:(NSUInteger)idx {      \
type##_t num = 0;                                                       \
[self getBytes:&num range:NSMakeRange(idx, sizeof(num))];               \
return num;                                                             \
}                                                                       \

@implementation NSData(APExtension)

+ (instancetype)dataWithInt8:(int8_t)integer { DATA_INT_IMP }
+ (instancetype)dataWithUInt8:(uint8_t)integer { DATA_INT_IMP }
+ (instancetype)dataWithInt16:(int16_t)integer { DATA_INT_IMP }
+ (instancetype)dataWithUInt16:(uint16_t)integer { DATA_INT_IMP }
+ (instancetype)dataWithInt32:(int32_t)integer { DATA_INT_IMP }
+ (instancetype)dataWithUInt32:(uint32_t)integer { DATA_INT_IMP }
+ (instancetype)dataWithInt64:(int64_t)integer { DATA_INT_IMP }
+ (instancetype)dataWithUInt64:(uint64_t)integer { DATA_INT_IMP }
+ (instancetype)dataWithUTF8String:(NSString *)string {
    return [string dataUsingEncoding:NSUTF8StringEncoding];
}

+ (instancetype)dataWithArray:(NSArray<NSData *> *)datas {
    NSMutableData *data = [NSMutableData data];
    [datas enumerateObjectsUsingBlock:^(NSData * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        [data appendData:obj];
    }];
    return [data copy];
}

+ (instancetype)dataWithFormat:(NSData *)format, ... {
    NSMutableArray *arguments = [NSMutableArray array];
    va_list args;
    va_start(args, format);
    if (format) {
        [arguments addObject:format];
        NSData *next = nil;
        while ((next = va_arg(args, NSData *))) {
            [arguments addObject:next];
        }
    }
    va_end(args);
    return [self dataWithArray:arguments];
}

+ (instancetype)dataWithHEX:(NSString *)hex {
    NSMutableData *data = [NSMutableData data];
    for (int i = 0; i < hex.length; i += 2) {
        NSString *hexStr = [hex substringWithRange:NSMakeRange(i, 2)];
        NSScanner *scanner = [NSScanner scannerWithString:hexStr];
        unsigned int intValue;
        [scanner scanHexInt:&intValue];
        [data appendBytes:&intValue length:1];
    }
    return [data copy];
}

+ (instancetype)dataWithLength:(NSUInteger)length {
    return [[NSMutableData dataWithLength:length] copy];
}

INT_DATA_IMP(int8)
INT_DATA_IMP(uint8)
INT_DATA_IMP(int16)
INT_DATA_IMP(uint16)
INT_DATA_IMP(int32)
INT_DATA_IMP(uint32)
INT_DATA_IMP(int64)
INT_DATA_IMP(uint64)

- (NSString *)utf8 {
    return [[NSString alloc] initWithData:self encoding:NSUTF8StringEncoding];
}

INT_DATA_IMP1(int8)
INT_DATA_IMP1(uint8)
INT_DATA_IMP1(int16)
INT_DATA_IMP1(uint16)
INT_DATA_IMP1(int32)
INT_DATA_IMP1(uint32)
INT_DATA_IMP1(int64)
INT_DATA_IMP1(uint64)

- (NSString *)hex {
    NSUInteger length = self.length;
    NSMutableString *result = [NSMutableString stringWithCapacity:length * 2];
    const unsigned char *byte = self.bytes;
    for (int i = 0; i < length; i++, byte++) {
        [result appendFormat:@"%02X", *byte];
    }
    return result;
}

- (NSData *)aes128EncryptWithKey:(NSData *)key {
    NSMutableData *encryptedData = [NSMutableData dataWithLength:self.length + kCCBlockSizeAES128];
    // 设置加密参数
    CCOptions options = kCCOptionECBMode | kCCOptionPKCS7Padding;
    
    // 执行加密
    size_t encryptedLength = 0;
    CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt, kCCAlgorithmAES, options,
                                          key.bytes, key.length,
                                          NULL,
                                          self.bytes, self.length,
                                          encryptedData.mutableBytes, encryptedData.length,
                                          &encryptedLength);
    
    if (cryptStatus == kCCSuccess) {
        encryptedData.length = encryptedLength;
        
        // 转换为十六进制字符串
        NSMutableString *hexString = [NSMutableString stringWithCapacity:(encryptedData.length * 2)];
        const unsigned char *encryptedBytes = encryptedData.bytes;
        for (int i = 0; i < encryptedData.length; i++) {
            [hexString appendFormat:@"%02X", encryptedBytes[i]];
        }
        // 补0至满32位
        while (hexString.length < 32) [hexString appendString:@"0"];
        return [NSData dataWithHEX:hexString];
    }
    
    return nil;
}

- (NSData *)aes128DecryptWithKey:(NSData *)key {
    NSString *hexString = self.hex;
    NSMutableData *encryptedData = [NSMutableData data];
    for (int i = 0; i < hexString.length; i += 2) {
        NSString *hexByte = [hexString substringWithRange:NSMakeRange(i, 2)];
        NSScanner *scanner = [NSScanner scannerWithString:hexByte];
        unsigned int byteValue = 0;
        [scanner scanHexInt:&byteValue];
        [encryptedData appendBytes:&byteValue length:1];
    }
    
    // 设置解密参数
    CCOptions options = kCCOptionECBMode | kCCOptionPKCS7Padding;
    
    // 执行解密
    NSMutableData *decryptedData = [NSMutableData dataWithLength:encryptedData.length + kCCBlockSizeAES128];
    size_t decryptedLength = 0;
    CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt, kCCAlgorithmAES, options,
                                          key.bytes, key.length,
                                          NULL,
                                          encryptedData.bytes, encryptedData.length,
                                          decryptedData.mutableBytes, decryptedData.length,
                                          &decryptedLength);
    
    if (cryptStatus == kCCSuccess) {
        decryptedData.length = decryptedLength;
        return decryptedData;
    }
    return nil;
}

@end

@implementation NSMutableData(APExtension)

- (NSData *)removeData:(NSUInteger)length {
    NSData *data = [self subdataWithRange:NSMakeRange(0, length)];
    [self replaceBytesInRange:NSMakeRange(0, length) withBytes:NULL length:0];
    return data;
}

@end

//
//  NSAttributedString+LSExtension.m
//  LPDoorBell
//
//  Created by Champion Fu on 2020/10/16.
//  Copyright © 2020 lancens_app. All rights reserved.
//

#import "NSAttributedString+LSExtension.h"
#ifdef ARPHA_PLUS
#import "ArphaPlus-Swift.h"
#elif CLDBUS
#import "Cldbus-Swift.h"
#else
#import "LNDU-Swift.h"
#endif

@implementation NSAttributedString (LSExtension)

+ (instancetype)attributedString:(id)string font:(id)font color:(UIColor *)color {
    if (APISAStr(string) || nil == string) return string;
    UIFont *f = [UIFont IDFont:font];
    return [[self alloc] initWithString:string attributes:@{NSFontAttributeName:f,NSForegroundColorAttributeName:color}];
}

+ (instancetype)attributedStringWithArray:(NSArray<NSAttributedString *> *)AStrings {
    NSMutableAttributedString *MAString = [[NSMutableAttributedString alloc] init];
    for (NSAttributedString *AString in AStrings) {
        [MAString appendAttributedString:AString];
    }
    return MAString;
}

- (CGFloat)heightForWidth:(CGFloat)width {
    if ([self length] == 0) return 0;
    return ceil([self.string sizeForFont:self.font size:CGSizeMake(width, HUGE) mode:NSLineBreakByWordWrapping].height);
}

- (CGFloat)width {
    if ([self length] == 0) return 0;
    return ceil([self.string sizeForFont:self.font size:CGSizeMake(HUGE, HUGE) mode:NSLineBreakByWordWrapping].width);
}

// 勾选 用户隐私协议文本  pop:是否是弹窗
+ (NSAttributedString *)userAgreementOn:(UIView *)view isPop:(BOOL)isPop {
    let text = isPop?@"Please read carefully After agreeing to all the contents of the \"User Agreement\" and \"Privacy Policy\" , you can start using our services".local:@"Please check the box that you have read and agreed to the \"User Agreement\" and \"Privacy Policy\"".local;
    let fs = @(isPop?-14.5:12.5);
    let attr = MAString(text, fs, APHEX(#DDDDDD)); // [UIColor labelColor]
    let attr1 = MAString(@"User Agreement".local, fs, [UIColor themeTextColor]);
    let attr2 = MAString(@"Privacy Policy".local, fs, [UIColor themeTextColor]);
    [attr setTextHighlightRange:[attr.string rangeOfString:attr1.string] color:[UIColor themeTextColor] backgroundColor:APLGrayColor tapAction:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
        [view.viewController pushVC:[FWWebViewController appWebViewWithLinkType:FWWebViewLinkTypeUserAgreement]];
    }];
    [attr setTextHighlightRange:[attr.string rangeOfString:attr2.string] color:[UIColor themeTextColor] backgroundColor:APClearColor tapAction:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
        [view.viewController pushVC:[FWWebViewController appWebViewWithLinkType:FWWebViewLinkTypePrivacyPolicy]];

    }];
    return attr;
}

+ (NSAttributedString *)customerService:(UIView *)view{
    let text = @"Having trouble logging in? Contact manual customer service".local;
    let fs = @(12);
    let attr = MAString(text, fs, APHEX(#98989F));
    let attr1 = MAString(@"customer service".local, fs, [UIColor themeTextColor]);
    [attr setTextHighlightRange:[attr.string rangeOfString:attr1.string] color:[UIColor themeTextColor] backgroundColor:APClearColor tapAction:^(UIView * _Nonnull containerView, NSAttributedString * _Nonnull text, NSRange range, CGRect rect) {
        [view.viewController pushVC:[FWWebViewController appWebViewWithLinkType:FWWebViewLinkTypeFaq]];
    }];
    return attr;
}



+ (NSArray<NSAttributedString *> *)strings:(id)titles fonts:(id)fonts colors:(id)colors {
    NSUInteger count = 0;
    if (APISArr(titles)) count = count?MIN([titles count], count):[titles count];
    if (APISArr(fonts)) count = count?MIN([fonts count], count):[fonts count];
    if (APISArr(colors)) count = count?MIN([colors count], count):[colors count];
    NSAssert(count > 0, @"parameters both not array");
    NSMutableArray *attrs = [NSMutableArray arrayWithCapacity:count];
    for (int i = 0; i < count; i++) {
        [attrs addObject:AString(ObjAt(titles, i), ObjAt(fonts, i), ObjAt(colors, i))];
    }
    return [attrs copy];
}

NS_INLINE id ObjAt(id obj, NSUInteger idx) {
    if (!APISArr(obj)) return obj;
    return [obj objectAtIndex:idx];
}

@end

//
//  NSAttributedString+LSExtension.h
//  LPDoorBell
//
//  Created by Champion Fu on 2020/10/16.
//  Copyright © 2020 lancens_app. All rights reserved.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

#define AString(t,f,c) [NSAttributedString attributedString:t font:f color:c]
#define AString1(...) [NSMutableAttributedString attributedStringWithArray:__VA_ARGS__]
#define AStrings(t,f,c) [NSAttributedString strings:t fonts:f colors:c]
#define MAString(t,f,c) [NSMutableAttributedString attributedString:t font:f color:c]


@interface NSAttributedString (LSExtension)

- (void)setGradientColors:(NSArray<UIColor *> *)colors;

+ (instancetype)attributedString:(nullable id)string font:(id)font color:(UIColor *)color;

+ (instancetype)attributedStringWithArray:(NSArray<NSAttributedString *> *)AStrings NS_SWIFT_NAME(init(strings:));

- (CGFloat)heightForWidth:(CGFloat)width;

- (CGFloat)width;

+ (NSAttributedString *)userAgreementOn:(UIView *)view isPop:(BOOL)isPop; // 用户协议
+ (NSAttributedString *)customerService:(UIView *)view; // 人工客服

+ (NSArray<NSAttributedString *> *)strings:(id)titles fonts:(nullable id)fonts colors:(nullable id)colors;

@end

NS_ASSUME_NONNULL_END

//
//  APTestView.m
//  ArphaPlus
//
//  Created by michael on 2023/12/21.
//

#import "NSArray+APExtension.h"

@implementation NSArray (APExtension)


- (BOOL)isEqualToOtherArray:(NSArray *)array {
    if (self == array) {
        NSLog(@"arrTag: is same");
        return YES;
    }
    if (![array isKindOfClass:[NSArray class]] || array.count != self.count) {
        NSLog(@"arrTag: type or count is not same");
        return NO;
    }
    for (int i = 0; i < self.count; i++) {
        if (![self[i] isEqual:array[i]]) {
            NSLog(@"arrTag: e1:%@, e2:%@ is not same",self[i], array[i]);
            return NO;
        }else {
            NSLog(@"arrTag: e1:%@, e2:%@ is same",self[i], array[i]);
        }
    }
    NSLog(@"arrTag: is same");
    return YES;
}

- (NSArray *)ap_appendArray:(NSArray *)array {
    if ([array count] == 0) return self;
    NSMutableArray *list = [NSMutableArray arrayWithArray:self];
    [list addObjectsFromArray:array];
    return [list copy];
}

- (NSArray *)ap_appendObject:(id)anObject {
    if (nil == anObject) return self;
    NSMutableArray *list = [NSMutableArray arrayWithArray:self];
    [list addObject:anObject];
    return [list copy];
}

@end

//
//  APTestView.h
//  ArphaPlus
//
//  Created by michael on 2023/12/21.
//

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN
@interface NSArray (APExtension)

- (BOOL)isEqualToOtherArray:(NSArray *)array;

- (NSArray *)ap_appendArray:(NSArray *)array;
- (NSArray *)ap_appendObject:(id)anObject;


@end

NS_ASSUME_NONNULL_END

//
//  Data+APExtension.swift
//  FuweiCloud
//
//  Created by michael on 2024/4/27.
//

import Foundation

extension NSData {
    var data: Data { self as Data }
}

extension Data {
    var ns: NSData { self as NSData }
    static func int8(_ value: Int8) -> Data { NSData(int8: value).data }
    static func uint8(_ value: UInt8) -> Data { NSData(uInt8: value).data }
    static func int16(_ value: Int16) -> Data { NSData(int16: value).data }
    static func uint16(_ value: UInt16) -> Data { NSData(uInt16: value).data }
    static func int32(_ value: Int32) -> Data { NSData(int32: value).data }
    static func uint32(_ value: UInt32) -> Data { NSData(uInt32: value).data }
    static func int64(_ value: Int64) -> Data { NSData(int64: value).data }
    static func uint64(_ value: UInt64) -> Data { NSData(uInt64: value).data }
    var int8: Int8 { ns.int8 }
    var uint8: UInt8 { ns.uint8 }
    var int16: Int16 { ns.int16 }
    var uint16: UInt16 { ns.uint16 }
    var int32: Int32 { ns.int32 }
    var uint32: UInt32 { ns.uint32 }
    var int64: Int64 { ns.int64 }
    var uint64: UInt64 { ns.uint64 }
    var hexString: String { ns.hexString()! }
    init(hex: String) {
        self.init(Array<UInt8>(hex: hex))
    }
    static func hex(_ hex: String) -> Self {
        NSData(hex: hex).data
    }
}

//
//  CAGradientLayer+APExtension.m
//  arpha plus
//
//  Created by michael on 2023/5/8.
//

#import "CAGradientLayer+APExtension.h"

@implementation CAGradientLayer (APExtension)

+ (instancetype)layerWithColors:(NSArray<UIColor *> *)colors type:(APLinearGradientType)type {
    CAGradientLayer *layer = [CAGradientLayer layer];
    CGPoint start;
    CGPoint end;
    switch (type) {
        case APLinearGradientType_TopToBottom:
        {
            start = CGPointMake(0, 0);
            end = CGPointMake(0, 1);
        }
            break;
        case APLinearGradientType_LeftToRight:
        {
            start = CGPointMake(0, 0);
            end = CGPointMake(1, 0);
        }
            break;
        case APLinearGradientType_LeftTopToRightBottom:
        {
            start = CGPointMake(0, 0);
            end = CGPointMake(1, 1);
        }
            break;
        case APLinearGradientType_LeftBottomToRightTop:
        {
            start = CGPointMake(0, 1);
            end = CGPointMake(1, 0);
        }
            break;
        default:
            break;
    }
    layer.startPoint = start;
    layer.endPoint = end;
    layer.colors = [colors map:^id _Nonnull(UIColor * _Nonnull obj) {
        return (__bridge id)obj.CGColor;
    }];
    return layer;
}

@end

//
//  CAGradientLayer+APExtension.h
//  arpha plus
//
//  Created by michael on 2023/5/8.
//

#import <QuartzCore/QuartzCore.h>

NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, APLinearGradientType) {
    APLinearGradientType_LeftToRight,
    APLinearGradientType_LeftTopToRightBottom,
    APLinearGradientType_TopToBottom,
    APLinearGradientType_LeftBottomToRightTop
};

@interface CAGradientLayer (APExtension)

+ (instancetype)layerWithColors:(NSArray<UIColor *> *)colors type:(APLinearGradientType)type;

@end

NS_ASSUME_NONNULL_END

//
//  Array+APExtension.swift
//  FuweiCloud
//
//  Created by michael on 2024/4/19.
//

import Foundation
import CoreGraphics

extension Array where Element == CGFloat {
    var size: CGSize {
        if count == 1 {
            return CGSizeMake(first!, first!)
        } else if count == 2 {
            return CGSizeMake(first!, last!)
        }
        return .zero
    }
    var rect: CGRect {
        if count == 2 {
            CGRectMake(0, 0, first!, last!)
        }else if count == 4 {
            CGRectMake(first!, self[1], self[2], last!)
        }else {
            .zero
        }
    }
    var point: CGPoint {
        if count == 1 {
            CGPointMake(first!, first!)
        }else if count == 2 {
            CGPointMake(first!, last!)
        }else {
            .zero
        }
    }
    var inset: UIEdgeInsets {
        if count == 1 {
            UIEdgeInsets(inset: first!)
        }else if count == 2 {
            UIEdgeInsets(horizontal: first!, vertical: last!)
        }else if count == 4 {
            UIEdgeInsets(top: first!, left: self[1], bottom: self[2], right: last!)
        }else {
            .zero
        }
    }
}


//
//  APFileManager+APExtension.swift
//  FuweiCloud
//
//  Created by michael on 2024/6/8.
//

import Foundation
import AVFoundation


extension APFileManager {
    @objc
    static func saveMedia(vid: String, timestamp: Int, image: UIImage?, completion: ((Bool) -> Void)? = nil) {
        let isVideo = image == nil
        let filePath = String(format: "%@/%@_%d.%@", APTools.albumPath(),vid,timestamp,(isVideo ?"mp4":"png"))
        saveMedia(filePath: filePath, image: image, completion: completion)
    }
    
    @objc
    static func saveMedia(filePath: String, image: UIImage?, completion: ((Bool) -> Void)? = nil) {
        if let image {
            ///< 写入图片到沙盒
            writeFile(atPath: filePath, content: image)
            ///< 写入相册
            APPhotosManager.saveMedia(toPhotoLibrary: URL(fileURLWithPath: filePath), completion: completion)
        }else {
            let videoAsset = AVURLAsset(url: URL(fileURLWithPath: filePath), options: [AVURLAssetPreferPreciseDurationAndTimingKey: false])
            /// < 写入时长
            let duration = Int32(videoAsset.duration.value) / videoAsset.duration.timescale
            xattrOfItem(atPath: filePath, setValue: String(duration), forKey: "duration")
            /// < 写入是否在app相册展示的标识字段
            xattrOfItem(atPath: filePath, setValue: "1", forKey: "AlbumFlag")
            ///< 写入封面
            let gen = AVAssetImageGenerator(asset: videoAsset)
            gen.appliesPreferredTrackTransform = true
            gen.maximumSize = CGSizeMake(100, 100)
            if let cgImage = try? gen.copyCGImage(at: CMTimeMakeWithSeconds(1, preferredTimescale: 600), actualTime: nil) { // 视频截图第一帧
                let fileName = filePath.lastPathComponent.deletingPathExtension
                let coverPath = String(format: "%@/%@.png", APTools.coverPath(),fileName)
                let coverImage = UIImage(cgImage: cgImage)
                writeFile(atPath: coverPath, content: coverImage)
            }
            
            ///< 写入相册
            APPhotosManager.saveMedia(toPhotoLibrary: URL(fileURLWithPath: filePath), completion: completion)
        }
    }
}

//
//  SwiftFunctions.swift
//  ArphaPlus
//
//  Created by michael on 2024/3/12.
//

import Foundation

#if ARPHA_PLUS

#elseif CLDBUS

#else

#endif

fileprivate extension Comparable {
    func clamped(_ low: Self, _ high: Self) -> Self {
        return max(low, min(self, high))
    }
}

func APSEL(_ sel: String) -> Selector {
    NSSelectorFromString(sel)
}

func APFont(_ font: Any) -> UIFont {
    if font is NSObject { ///< ID类型
        UIFont.idFont(font)
    }else if let font = font as? (any BinaryInteger) { ///< 整型
        UIFont.numFont(Float(font))
    }else if let font = font as? (any BinaryFloatingPoint) { ///< 浮点型
        UIFont.numFont(Float(font))
    }else {
        UIFont()
    }
}

func APClamp<T: Comparable>(_ x: T, _ low: T, _ high: T) -> T {
    x.clamped(low, high)
}

func APString(_ format: String, _ args: CVarArg ...) -> String {
    String(format: format, args)
}

// Color
func APRGB(_ r: Double, _ g: Double, _ b: Double, _ a: Double = 1.0) -> UIColor {
    UIColor(red: r/255, green: g/255, blue: b/255, alpha: a)
}

func APHEX(_ hex: String) -> UIColor {
    UIColor(hexString: hex)!
}

func APGray1(_ value: String) -> UIColor {
    UIColor(hexString: String(repeating: value, count: 6))!
}

func APGray2(_ value: String) -> UIColor {
    UIColor(hexString: String(repeating: value, count: 3))!
}

func APGray3(_ value: CGFloat) -> UIColor {
    APRGB(value.double, value.double, value.double)
}


// APMakeToast
func APMakeToast(_ msg: String, _ duration: TimeInterval = 1.5) {
    APTools.makeToast(msg, duration: duration)
}

// APHideHUD
func APShowHUD(message: [APPopProperty: Any], on: UIView? = nil) {
    APTools.showHUD(message, on: on)
}

func APShowHUD(_ msg: Any? = nil, timeout: Double = 0, on: UIView? = nil, _ timeoutBlock: APVoidBlock? = nil) {
    APTools.showHUD(msg, timeout: timeout, on: on, block: timeoutBlock)
}

func APMakeHUD(_ msg: Any?, state: Bool, on: UIView? = nil) { // state: true-成功 false-失败
    APTools.makeHUD(msg, state: state, on: on)
}

func APHideHUD(_ on: UIView? = nil, animated: Bool = false) {
    APPopView.dismiss(on: on, animated: animated)
}

// UIImageView
func APImageViewNamed(_ named:String) -> UIImageView? {
    UIImageView(named: named)
}

// UIImage
func APImageNamed(_ named:String) -> UIImage? {
    UIImage(named: named) ?? nil
}

// Adapter
func FWAdapter<T>(_ values: [T]) -> T {
    guard values.count == 3 else {
        fatalError("FWUIAdapter values error")
    }
    return if FW_ARPHA_FLAG {
        values.first!
    }
    else if FW_CLDBUS_FLAG {
        values[1]
    }
    else {
        values.last!
    }
}

//
//  SwiftConstants.swift
//  ArphaPlus
//
//  Created by michael on 2024/3/12.
//

import UIKit
import PPCS.APP2PManager
import VLink.APBLEManager

#if ARPHA_PLUS
let FW_APP_FLAG = 0
let logSubsystemName = "app"
let logCategoryName = "arpha_plus"
typealias FWDeviceListViewCell = APDeviceListViewCell
typealias FWDeviceEmptyView = APDeviceEmptyView
typealias FWLoginViewController = APLoginViewController
typealias FWLaunchScreenView = APLaunchScreenView
#elseif CLDBUS
let FW_APP_FLAG = 1
let logSubsystemName = "app"
let logCategoryName = "cldbus"
typealias FWDeviceListViewCell = CLDeviceListViewCell
typealias FWDeviceEmptyView = CLDeviceEmptyView
typealias FWLoginViewController = APLoginViewController
typealias FWLaunchScreenView = APLaunchScreenView
#else
let FW_APP_FLAG = 2
let logSubsystemName = "app"
let logCategoryName = "lndu"
typealias FWDeviceListViewCell = CLDeviceListViewCell
typealias FWDeviceEmptyView = LNDeviceEmptyView
typealias FWLoginViewController = LNLoginViewController
typealias FWLaunchScreenView = LNLaunchScreenView
#endif

typealias AString = NSAttributedString
typealias MAString = NSMutableAttributedString
typealias APVoidBlock = () -> Void
typealias APBoolBlock = (Bool) -> Void
typealias APIntBlock = (Int) -> Void
typealias FWAnyBlock = (Any?) -> Void
typealias APAnyBlock = (Any) -> Void

var googleClientId = ""
var googleAuthCode = ""
let GoogleAssistant = "Google" // Assistant
let AmazonAlexa = "Amazon Alexa"
var APScreenW: CGFloat { UIScreen.main.bounds.width } ///< 计算属性，根据屏幕旋转状态不同，结果也不同
var APScreenH: CGFloat { UIScreen.main.bounds.height }
var isLandscape: Bool { // 当前屏幕方向是否横屏  APScreenW > APScreenH
    if #available(iOS 16.0, *) {
        return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation.isLandscape ?? false
    } else {
         //return UIApplication.shared.statusBarOrientation.isLandscape
        return UIDevice.current.orientation.isLandscape
    }
}
let APApplication = UIApplication.shared
let NotiCenter = NotificationCenter.default
let APAppDelegate = UIApplication.shared.delegate as! FWAppDelegate
let APUserDefaults = UserDefaults.standard
let APKeyWindow = UIApplication.shared.keyWindow!
let APKeyWindowView = APKeyWindow.rootViewController!.view
let APSafeAreaInsets = APKeyWindow.safeAreaInsets
let APisIPhoneX = APSafeAreaInsets.bottom > 0
let APSafeBottomHeight: CGFloat = APisIPhoneX ?34:0
let APStatusBarHeight: CGFloat = APisIPhoneX ?44:20
let APNaviBarHeight: CGFloat = APStatusBarHeight + 44
let APTabBarHeight: CGFloat = APisIPhoneX ?84:50
let APGlobal = APGlobalObject.shared()
let APParam = APParamObject.shared()
let CellInset: CGFloat = APScreenW > 375 ?20:16
let SharedBLEManager = APBLEManager.shared()
let SharedP2PManager = APP2PManager.shared()
let FW_ARPHA_FLAG = FW_APP_FLAG == 0
let FW_CLDBUS_FLAG = FW_APP_FLAG == 1
let FW_LNDU_FLAG = FW_APP_FLAG == 2

//
//  FWFFmpegSupport.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/9.
//

import ffmpegkit

class FWFFmpegSupport {
    private static let processQueue = DispatchQueue(label: "ffmpeg.process.queue", qos: .default, attributes: .concurrent)
    
    private static func command(_ command: String) -> Bool {
        ReturnCode.isSuccess(FFmpegKit.execute(command).getReturnCode())
    }
    
    static func recomposeVideo(videoPath: String, completion: @escaping (Bool) -> Void) {
        processQueue.async {
            let path = videoPath.fileURL.absoluteString
            let tmpDir = NSTemporaryDirectory()
            let aacPath = (tmpDir + "/out_aac.aac").fileURL
            let h264Path = (tmpDir + "/out_h264.mp4").fileURL
            let outputPath = (tmpDir + "/out_vid.mp4").fileURL
            
            if !command("-i \(path) -y \(aacPath.absoluteString)") {
                print("============mp3 convert aac from mp4 error=============")
                completion(false)
                return
            }else if !command("-i \(path) -y \(aacPath.absoluteString)") { /// 将音频分离并编码为aac
                print("============mp3 convert aac from mp4 error=============")
                completion(false)
                return
            }else if !command("-i \(path) -vcodec copy -an -y \(h264Path.absoluteString)") { ///  将视频分离
                print("============h264 seperate from mp4 error=============")
                completion(false)
                return
            }else if !command("-i \(h264Path.absoluteString) -i \(aacPath.absoluteString) -y \(outputPath.absoluteString)") { /// 重新合成MP4
                print("============h264 + aac compose mp4 error=============")
                completion(false)
                return
            }else {
                /// 处理完成的后覆盖原来的视频
                APFileManager.moveItem(atPath: outputPath.path, toPath: videoPath, overwrite: true)
                print("============h264 + aac compose mp4 success=============")
                completion(true)
            }
        }
    }
    
    static func rotateVideo(videoPath: String, rotation: Int, completion: @escaping (Bool) -> Void) {
        processQueue.async {
            let path = videoPath.fileURL.absoluteString
            let tmpDir = NSTemporaryDirectory()
            let rotatePath = (tmpDir + "/out_rotate.mp4").fileURL
            if !command("-i \(path) -c copy -metadata:s:v:0 rotate=\(rotation) -y \(rotatePath.absoluteString)") {
                print("============mp4 rotation error=============")
                completion(false)
                return
            }else {
                print("============mp4 rotation success=============")
                /// 处理完成的后覆盖原来的视频
                APFileManager.moveItem(atPath: rotatePath.path, toPath: videoPath, overwrite: true)
                completion(true)
            }
        }
    }
}

//
//  PPCS+FWExtension.swift
//  FuweiCloud
//
//  Created by michael on 2024/9/6.
//

import Foundation
import PPCS

extension APP2PManager {
    @objc func connect(device: APDeviceModel, delegate: (any APP2PManagerDelegate)?) {
        audioPacketSize = device.audioBytesPerPacket()
        connect(withDid: device.did, delegate: delegate)
    }
    
    @objc func sendUnlockCommand(device: APDeviceModel, completion: @escaping (Bool) -> Void) {
        if device.iot != 0 {
            APIoTModelRequest.unlock(device.vid, completion: completion)
        }else {
            send(.unlock, data: ["member_id":APGlobal.userInfo!.id,"t":UInt64(Date().timeIntervalSince1970*1000)]) { results, success in
                if let results, JSON(results)["result"].string == "success", JSON(results)["error_code"].int == 0 {
                    completion(true)
                }else {
                    completion(false)
                }
            }
        }
    }
}

//
//  FWLocationManager.swift
//  FuweiCloud
//
//  Created by michael on 2024/7/4.
//

import Foundation
import CoreLocation
import SystemConfiguration.CaptiveNetwork

class FWLocationManager: NSObject, CLLocationManagerDelegate {
    
    @objc static let shared = FWLocationManager()
    
    private let locationManager = CLLocationManager()
    private var confirmBlock: APVoidBlock? = nil
    private var retryCount: Int = 0
    private let maxCount: Int = 2
    
    @objc func requestLocation(_ confirmBlock: APVoidBlock? = nil) -> Bool {
        var title: String? = nil
        let status = if #available(iOS 14.0, *) {
            locationManager.authorizationStatus
        }else {
            CLLocationManager.authorizationStatus()
        }
        if status == .notDetermined {
            locationManager.requestWhenInUseAuthorization()
            locationManager.delegate = self
            self.confirmBlock = confirmBlock
            return false
        }
        if #available(iOS 13.0, *) {
            if status == .restricted || status == .denied {
                title = "We need to access your location permission to get WiFi information on iOS 13.0 and above. WiFi information is used to configure the network for your IoT device. We will not use your location information for other unauthorized purposes".local
            }
        }
        if #available(iOS 14.0, *) {
            if locationManager.accuracyAuthorization == .reducedAccuracy {
                title = "We need to access your precise location permission so that we can get WiFi information on iOS14.0 and above systems. WiFi information is used to configure the network for your IoT device".local
            }
        }
        if let title {
            APConfirmPopView.pop(withUserInfo: [
                APPopProperty.header: title,
                APPopProperty.rightButton: "Setting".local
            ]) { userInfo in
                guard let userInfo else { return }
                APAppSettingURLHelper.gotoURL(.appSetting)
            }
        }
        return nil == title
    }
    
    
    @objc func getWiFiName(completion: @escaping (String?) -> Void) {
        if let name = getWiFiName() {
            retryCount = 0
            completion(name)
        } else if retryCount < maxCount {
            dispatch_after_on_main_queue(1) {
                self.getWiFiName(completion: completion)
            }
        }else {
            retryCount = 0
            completion(nil)
        }
    }
    
    @objc func getWiFiName() -> String? {
        var ssid: String?
        if let interfaces = CNCopySupportedInterfaces() as NSArray? {
            for interface in interfaces {
                if let interfaceInfo = CNCopyCurrentNetworkInfo(interface as! CFString) as NSDictionary? {
                    ssid = interfaceInfo[kCNNetworkInfoKeySSID as String] as? String
                    break
                }
            }
        }
        return ssid
    }
    
    @available(iOS 14.0, *)
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        let status = manager.authorizationStatus
        if status == .authorizedWhenInUse && manager.accuracyAuthorization == .fullAccuracy {
            confirmBlock?()
            confirmBlock = nil
        }
    }
    func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
        if status == .authorizedWhenInUse {
            confirmBlock?()
            confirmBlock = nil
        }
    }
    
}

//
//  MJCodingModel.swift
//  FuweiCloud
//
//  Created by michael on 2024/6/28.
//

import Foundation


///  用于实现自定义对象的归档和解档
class MJCodingModel: NSObject, NSSecureCoding {
    static var supportsSecureCoding: Bool { true }
    
    override init() {
        super.init()
    }
    
    func encode(with coder: NSCoder) {
        self.mj_encode(coder)
    }
    
    required convenience init?(coder: NSCoder) {
        self.init()
        self.mj_decode(coder)
    }
}

/*
 * Copyright (c) 2000-2008 Apple Inc. All rights reserved.
 *
 * @APPLE_OSREFERENCE_LICENSE_HEADER_START@
 *
 * This file contains Original Code and/or Modifications of Original Code
 * as defined in and that are subject to the Apple Public Source License
 * Version 2.0 (the 'License'). You may not use this file except in
 * compliance with the License. The rights granted to you under the License
 * may not be used to create, or enable the creation or redistribution of,
 * unlawful or unlicensed copies of an Apple operating system, or to
 * circumvent, violate, or enable the circumvention or violation of, any
 * terms of an Apple operating system software license agreement.
 *
 * Please obtain a copy of the License at
 * http://www.opensource.apple.com/apsl/ and read it before using this file.
 *
 * The Original Code and all software distributed under the License are
 * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
 * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
 * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
 * Please see the License for the specific language governing rights and
 * limitations under the License.
 *
 * @APPLE_OSREFERENCE_LICENSE_HEADER_END@
 */
/*
 * Copyright (c) 1980, 1986, 1993
 *    The Regents of the University of California.  All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *    This product includes software developed by the University of
 *    California, Berkeley and its contributors.
 * 4. Neither the name of the University nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED.  IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 *
 *    @(#)route.h    8.3 (Berkeley) 4/19/94
 * $FreeBSD: src/sys/net/route.h,v 1.36.2.1 2000/08/16 06:14:23 jayanth Exp $
 */

#ifndef _NET_ROUTE_H_
#define    _NET_ROUTE_H_
#include <sys/appleapiopts.h>
#include <stdint.h>
#include <sys/types.h>
#include <sys/socket.h>

/*
 * These numbers are used by reliable protocols for determining
 * retransmission behavior and are included in the routing structure.
 */
struct rt_metrics {
    u_int32_t    rmx_locks;    /* Kernel leaves these values alone */
    u_int32_t    rmx_mtu;    /* MTU for this path */
    u_int32_t    rmx_hopcount;    /* max hops expected */
    int32_t        rmx_expire;    /* lifetime for route, e.g. redirect */
    u_int32_t    rmx_recvpipe;    /* inbound delay-bandwidth product */
    u_int32_t    rmx_sendpipe;    /* outbound delay-bandwidth product */
    u_int32_t    rmx_ssthresh;    /* outbound gateway buffer limit */
    u_int32_t    rmx_rtt;    /* estimated round trip time */
    u_int32_t    rmx_rttvar;    /* estimated rtt variance */
    u_int32_t    rmx_pksent;    /* packets sent using this route */
    u_int32_t    rmx_filler[4];    /* will be used for T/TCP later */
};

/*
 * rmx_rtt and rmx_rttvar are stored as microseconds;
 */
#define    RTM_RTTUNIT    1000000    /* units for rtt, rttvar, as units per sec */



#define    RTF_UP        0x1        /* route usable */
#define    RTF_GATEWAY    0x2        /* destination is a gateway */
#define    RTF_HOST    0x4        /* host entry (net otherwise) */
#define    RTF_REJECT    0x8        /* host or net unreachable */
#define    RTF_DYNAMIC    0x10        /* created dynamically (by redirect) */
#define    RTF_MODIFIED    0x20        /* modified dynamically (by redirect) */
#define    RTF_DONE    0x40        /* message confirmed */
#define    RTF_DELCLONE    0x80        /* delete cloned route */
#define    RTF_CLONING    0x100        /* generate new routes on use */
#define    RTF_XRESOLVE    0x200        /* external daemon resolves name */
#define    RTF_LLINFO    0x400        /* generated by link layer (e.g. ARP) */
#define    RTF_STATIC    0x800        /* manually added */
#define    RTF_BLACKHOLE    0x1000        /* just discard pkts (during updates) */
#define    RTF_NOIFREF    0x2000        /* not eligible for RTF_IFREF */
#define    RTF_PROTO2    0x4000        /* protocol specific routing flag */
#define    RTF_PROTO1    0x8000        /* protocol specific routing flag */

#define    RTF_PRCLONING    0x10000        /* protocol requires cloning */
#define    RTF_WASCLONED    0x20000        /* route generated through cloning */
#define    RTF_PROTO3    0x40000        /* protocol specific routing flag */
/* 0x80000 unused */
#define    RTF_PINNED    0x100000    /* future use */
#define    RTF_LOCAL    0x200000    /* route represents a local address */
#define    RTF_BROADCAST    0x400000    /* route represents a bcast address */
#define    RTF_MULTICAST    0x800000    /* route represents a mcast address */
#define    RTF_IFSCOPE    0x1000000    /* has valid interface scope */
#define    RTF_CONDEMNED    0x2000000    /* defunct; no longer modifiable */
#define    RTF_IFREF    0x4000000    /* route holds a ref to interface */
#define    RTF_PROXY    0x8000000    /* proxying, no interface scope */
#define    RTF_ROUTER    0x10000000    /* host is a router */
/* 0x20000000 and up unassigned */

#define    RTF_BITS \
"\020\1UP\2GATEWAY\3HOST\4REJECT\5DYNAMIC\6MODIFIED\7DONE" \
"\10DELCLONE\11CLONING\12XRESOLVE\13LLINFO\14STATIC\15BLACKHOLE" \
"\16NOIFREF\17PROTO2\20PROTO1\21PRCLONING\22WASCLONED\23PROTO3" \
"\25PINNED\26LOCAL\27BROADCAST\30MULTICAST\31IFSCOPE\32CONDEMNED" \
"\33IFREF\34PROXY\35ROUTER"

/*
 * Routing statistics.
 */
struct    rtstat {
    short    rts_badredirect;    /* bogus redirect calls */
    short    rts_dynamic;        /* routes created by redirects */
    short    rts_newgateway;        /* routes modified by redirects */
    short    rts_unreach;        /* lookups which failed */
    short    rts_wildcard;        /* lookups satisfied by a wildcard */
};

/*
 * Structures for routing messages.
 */
struct rt_msghdr {
    u_short    rtm_msglen;    /* to skip over non-understood messages */
    u_char    rtm_version;    /* future binary compatibility */
    u_char    rtm_type;    /* message type */
    u_short    rtm_index;    /* index for associated ifp */
    int    rtm_flags;    /* flags, incl. kern & message, e.g. DONE */
    int    rtm_addrs;    /* bitmask identifying sockaddrs in msg */
    pid_t    rtm_pid;    /* identify sender */
    int    rtm_seq;    /* for sender to identify action */
    int    rtm_errno;    /* why failed */
    int    rtm_use;    /* from rtentry */
    u_int32_t rtm_inits;    /* which metrics we are initializing */
    struct rt_metrics rtm_rmx; /* metrics themselves */
};

struct rt_msghdr2 {
    u_short    rtm_msglen;    /* to skip over non-understood messages */
    u_char    rtm_version;    /* future binary compatibility */
    u_char    rtm_type;    /* message type */
    u_short    rtm_index;    /* index for associated ifp */
    int    rtm_flags;    /* flags, incl. kern & message, e.g. DONE */
    int    rtm_addrs;    /* bitmask identifying sockaddrs in msg */
    int32_t    rtm_refcnt;    /* reference count */
    int    rtm_parentflags; /* flags of the parent route */
    int    rtm_reserved;    /* reserved field set to 0 */
    int    rtm_use;    /* from rtentry */
    u_int32_t rtm_inits;    /* which metrics we are initializing */
    struct rt_metrics rtm_rmx; /* metrics themselves */
};


#define    RTM_VERSION    5    /* Up the ante and ignore older versions */

/*
 * Message types.
 */
#define    RTM_ADD        0x1    /* Add Route */
#define    RTM_DELETE    0x2    /* Delete Route */
#define    RTM_CHANGE    0x3    /* Change Metrics or flags */
#define    RTM_GET        0x4    /* Report Metrics */
#define    RTM_LOSING    0x5    /* Kernel Suspects Partitioning */
#define    RTM_REDIRECT    0x6    /* Told to use different route */
#define    RTM_MISS    0x7    /* Lookup failed on this address */
#define    RTM_LOCK    0x8    /* fix specified metrics */
#define    RTM_OLDADD    0x9    /* caused by SIOCADDRT */
#define    RTM_OLDDEL    0xa    /* caused by SIOCDELRT */
#define    RTM_RESOLVE    0xb    /* req to resolve dst to LL addr */
#define    RTM_NEWADDR    0xc    /* address being added to iface */
#define    RTM_DELADDR    0xd    /* address being removed from iface */
#define    RTM_IFINFO    0xe    /* iface going up/down etc. */
#define    RTM_NEWMADDR    0xf    /* mcast group membership being added to if */
#define    RTM_DELMADDR    0x10    /* mcast group membership being deleted */
#define    RTM_IFINFO2    0x12    /* */
#define    RTM_NEWMADDR2    0x13    /* */
#define    RTM_GET2    0x14    /* */

/*
 * Bitmask values for rtm_inits and rmx_locks.
 */
#define    RTV_MTU        0x1    /* init or lock _mtu */
#define    RTV_HOPCOUNT    0x2    /* init or lock _hopcount */
#define    RTV_EXPIRE    0x4    /* init or lock _expire */
#define    RTV_RPIPE    0x8    /* init or lock _recvpipe */
#define    RTV_SPIPE    0x10    /* init or lock _sendpipe */
#define    RTV_SSTHRESH    0x20    /* init or lock _ssthresh */
#define    RTV_RTT        0x40    /* init or lock _rtt */
#define    RTV_RTTVAR    0x80    /* init or lock _rttvar */

/*
 * Bitmask values for rtm_addrs.
 */
#define    RTA_DST        0x1    /* destination sockaddr present */
#define    RTA_GATEWAY    0x2    /* gateway sockaddr present */
#define    RTA_NETMASK    0x4    /* netmask sockaddr present */
#define    RTA_GENMASK    0x8    /* cloning mask sockaddr present */
#define    RTA_IFP        0x10    /* interface name sockaddr present */
#define    RTA_IFA        0x20    /* interface addr sockaddr present */
#define    RTA_AUTHOR    0x40    /* sockaddr for author of redirect */
#define    RTA_BRD        0x80    /* for NEWADDR, broadcast or p-p dest addr */

/*
 * Index offsets for sockaddr array for alternate internal encoding.
 */
#define    RTAX_DST    0    /* destination sockaddr present */
#define    RTAX_GATEWAY    1    /* gateway sockaddr present */
#define    RTAX_NETMASK    2    /* netmask sockaddr present */
#define    RTAX_GENMASK    3    /* cloning mask sockaddr present */
#define    RTAX_IFP    4    /* interface name sockaddr present */
#define    RTAX_IFA    5    /* interface addr sockaddr present */
#define    RTAX_AUTHOR    6    /* sockaddr for author of redirect */
#define    RTAX_BRD    7    /* for NEWADDR, broadcast or p-p dest addr */
#define    RTAX_MAX    8    /* size of array to allocate */

struct rt_addrinfo {
    int    rti_addrs;
    struct    sockaddr *rti_info[RTAX_MAX];
};


#endif /* _NET_ROUTE_H_ */

//
//  getgateway.h
//  MSTEnterprise
//
//  Created by chuting  on 13-4-24.
//  Copyright (c) 2013年 chuting . All rights reserved.
//



#ifndef MSTEnterprise_getgateway_h
#define MSTEnterprise_getgateway_h
#include <TargetConditionals.h>


// 模拟器调试里可以编译通过并执行，而选真机调试时却在编译中显示该文件找不到 net/route.h

#if  TARGET_IPHONE_SIMULATOR
//#include <net/route.h>
#include "route.h"
#elif TARGET_OS_IPHONE
#include "route.h"
#endif



 

unsigned char * getdefaultgateway(in_addr_t * addr);

#endif

//
//  getgateway.c
//  MSTEnterprise
//
//  Created by chuting  on 13-4-24.
//  Copyright (c) 2013年 chuting . All rights reserved.
//

#include <stdio.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <sys/sysctl.h>
#include "getgateway.h"
#include <net/if.h>
#include <string.h>
#include <TargetConditionals.h>
 

#define CTL_NET         4


#if defined(BSD) || defined(__APPLE__)

#define ROUNDUP(a) \
((a) > 0 ? (1 + (((a) - 1) | (sizeof(long) - 1))) : sizeof(long))

int getLocalIP(in_addr_t * addr)
{
    int mib[] = {CTL_NET, PF_ROUTE, 0, AF_INET,
        NET_RT_FLAGS, RTF_GATEWAY};
    size_t l;
    char * buf, * p;
    struct rt_msghdr * rt;
    struct sockaddr * sa;
    struct sockaddr * sa_tab[RTAX_MAX];
    int i;
    int r = -1;
    if(sysctl(mib, sizeof(mib)/sizeof(int), 0, &l, 0, 0) < 0) {
        return -1;
    }
    if(l>0) {
        buf = malloc(l);
        if(sysctl(mib, sizeof(mib)/sizeof(int), buf, &l, 0, 0) < 0) {
            return -1;
        }
        for(p=buf; p<buf+l; p+=rt->rtm_msglen) {
            rt = (struct rt_msghdr *)p;
            sa = (struct sockaddr *)(rt + 1);
            for(i=0; i<RTAX_MAX; i++) {
                if(rt->rtm_addrs & (1 << i)) {
                    sa_tab[i] = sa;
                    sa = (struct sockaddr *)((char *)sa + ROUNDUP(sa->sa_len));
                } else {
                    sa_tab[i] = NULL;
                }
            }
            
            if( ((rt->rtm_addrs & (RTA_DST|RTA_GATEWAY)) == (RTA_DST|RTA_GATEWAY))
               && sa_tab[RTAX_DST]->sa_family == AF_INET
               && sa_tab[RTAX_GATEWAY]->sa_family == AF_INET) {
                
                
                if(((struct sockaddr_in *)sa_tab[RTAX_DST])->sin_addr.s_addr == 0) {
                    char ifName[128];
                    if_indextoname(rt->rtm_index,ifName);
                    
                    if(strcmp("en0",ifName)==0){
                        
                        *addr = ((struct sockaddr_in *)(sa_tab[RTAX_GATEWAY]))->sin_addr.s_addr;
                        r = 0;
                    }
                }
            }
        }
        free(buf);
    }
    return r;
}



unsigned char * getdefaultgateway(in_addr_t * addr)
{
    unsigned char * octet=malloc(4);
#if 0
    /* net.route.0.inet.dump.0.0 ? */
    int mib[] = {CTL_NET, PF_ROUTE, 0, AF_INET,
        NET_RT_DUMP, 0, 0/*tableid*/};
#endif
    /* net.route.0.inet.flags.gateway */
    int mib[] = {CTL_NET, PF_ROUTE, 0, AF_INET,
        NET_RT_FLAGS, RTF_GATEWAY};
    size_t l;
    char * buf, * p;
    struct rt_msghdr * rt;
    struct sockaddr * sa;
    struct sockaddr * sa_tab[RTAX_MAX];
    int i;
    if(sysctl(mib, sizeof(mib)/sizeof(int), 0, &l, 0, 0) < 0) {
        return octet;
    }
    if(l>0) {
        buf = malloc(l);
        if(sysctl(mib, sizeof(mib)/sizeof(int), buf, &l, 0, 0) < 0) {
            return octet;
        }
        for(p=buf; p<buf+l; p+=rt->rtm_msglen) {
            rt = (struct rt_msghdr *)p;
            sa = (struct sockaddr *)(rt + 1);
            for(i=0; i<RTAX_MAX; i++) {
                if(rt->rtm_addrs & (1 << i)) {
                    sa_tab[i] = sa;
                    sa = (struct sockaddr *)((char *)sa + ROUNDUP(sa->sa_len));
                } else {
                    sa_tab[i] = NULL;
                }
            }
            
            if( ((rt->rtm_addrs & (RTA_DST|RTA_GATEWAY)) == (RTA_DST|RTA_GATEWAY))
               && sa_tab[RTAX_DST]->sa_family == AF_INET
               && sa_tab[RTAX_GATEWAY]->sa_family == AF_INET) {
                
             
                for (int i=0; i<4; i++){
                    octet[i] = ( ((struct sockaddr_in *)(sa_tab[RTAX_GATEWAY]))->sin_addr.s_addr >> (i*8) ) & 0xFF;
                }
                
            }
        }
        free(buf);
    }
    return octet;
}
#endif

//
//  APBLEManager+Peripheral.swift
//  FuweiCloud
//
//  Created by michael on 2024/8/7.
//

import VLink.APBLEManager

extension APBLEDeviceMatchType {
    
    private static func mac() -> Data {
        Data()
    }
    var advertisementData: [String: Any] {
        switch self {
        case .D288: [:]
        default: [:]
        }
    }
}

extension APBLEManager {
    
    /// 检测蓝牙状态
    @discardableResult
    @objc func checkBLEState() -> Bool {
        switch bleState {
        case .unsupported:
            APToastPopView.forcePop([.header:"Bluetooth not available".local])
            return false
        case .unauthorized:
            APConfirmPopView.pop(withUserInfo: [
                APPopProperty.header: "Bluetooth Unauthorized".local,
                APPopProperty.body: "Please go to the phone settings to grant Bluetooth permissions to the app".local,
                APPopProperty.rightButton: "Setting".local
            ]) { userInfo in
                if nil == userInfo { return }
                APAppSettingURLHelper.gotoURL(.appSetting)
            }
            return false
        case .poweredOff:
            APConfirmPopView.pop(withUserInfo: [
                APPopProperty.header: "Bluetooth is not turned on or new connections are prohibited".local,
                APPopProperty.body: "Please go to the \"Settings\" on your phone to turn on the Bluetooth switch or allow Bluetooth to make a new connection".local,
                APPopProperty.rightButton: "Setting".local
            ]) { userInfo in
                if nil == userInfo { return }
                APAppSettingURLHelper.gotoURL(.BLE)
            }
            return false
        default:
            return true
        }
    }
    
    @discardableResult
    @objc func popDisconnect(_ device: APDeviceModel, showHUD: Bool = true, reason: APBLEDisconnectReason = .none, cancelBlock: APVoidBlock? = nil, connectBlock: APVoidBlock? = nil, successBlock: APVoidBlock? = nil) -> Bool {
        guard checkBLEState() else { return false }
        if SharedBLEManager.isConnecting {
            if showHUD {
                APShowHUD("Bluetooth connecting...".local, timeout: 10)
            }
            return false
        }
        if !SharedBLEManager.isConnected { // 弹窗
            let reasonStr = if reason == .secretInvalid {
                ("Bluetooth key invalid, please reset device and rebind".local, "Exit".local)
            } else if reason == .unbinded {
                ("The device has been unbound, please rebind it".local, "Exit".local)
            } else {
                ("Device disconnection exception, please reconnect".local, "Reconnect".local)
            }
            guard reason != .otaUpgrade else { return false }
            APConfirmPopView.pop(withUserInfo: [
                APPopProperty.header: "Bluetooth connection disconnected".local,
                APPopProperty.body: reasonStr.0,
                APPopProperty.rightButton: reasonStr.1
            ]) { [weak self] idx in
                guard let self else { return }
                if idx as? Int == 1 {
                    if reason == .none {
                        if showHUD {
                            APShowHUD("Bluetooth connecting...".local, timeout: 10)
                        }
                        SharedBLEManager.connect(withName: device.bluetooth_id, config: device.bt_config.keyValues, encryptKey: device.encryptKey(), timeout: 10) { status in
                            self.handleConnectAlert(status: status, showHUD: showHUD, successBlock: successBlock)
                        }
                    }
                    connectBlock?()
                } else {
                    if reason != .none {
                        cancelBlock?()
                    }
                }
            }
        }
        return SharedBLEManager.isConnected
    }
    
    func handleConnectAlert(status: APBLEDeviceConnectStatus, showHUD: Bool, successBlock: APVoidBlock? = nil) {
        DDLogError("handleConnectAlert: status:\(status), showHUD:\(showHUD)")
        if status != .success {
            if showHUD { APPopView.dismiss(false) }
            stopDiscover()
            dispatch_after_on_main_queue(0.1) {
                let title = (status == .failure ?"Connection failed":"Connection timeout").local
                APToastPopView.forcePop([.header: title,
                                         .bodyAlign: 0,
                                         .body: "Please investigate the following methods before trying again\n1. Please bring your phone close to the device and try again\n2. Please check if the power supply of the device is sufficient\n3. Please check if the device has been reset and needs to be re paired".local])
            }
        }else {
            if let successBlock {
                successBlock()
            }else {
                APHideHUD()
            }
        }
    }
}


//
//  APBLEParameterExtension.m
//  FuweiCloud
//
//  Created by michael on 2024/9/2.
//

#import "APBLEParameter.h"

MJSecureCodingImplementation(APLockPowerGetResp, YES)
MJSecureCodingImplementation(APLockAllAttrResp, YES)
MJSecureCodingImplementation(APAutoReplyGetResp, YES)
MJSecureCodingImplementation(APLockVersionInfoResp, YES)



#import <Foundation/Foundation.h>

@interface APAudioUnitManager : NSObject


#define SharedAUManager [APAudioUnitManager sharedManager]



+ (instancetype)sharedManager;

@property (assign, nonatomic, readonly) BOOL isRecording;
@property (assign, nonatomic, readonly) BOOL isPlaying;
@property (nonatomic, assign) int recordFactor;
@property (nonatomic, assign) int sampleRate;
@property (nonatomic, assign) BOOL echoCancellationEnabled;
@property (nonatomic, copy, nullable) void(^recordBufferCallback)(uint8_t *buffer, int length);
@property (nonatomic, copy, nullable) void(^volumeCallback)(float volume);

- (void)startAudioUnit;
- (void)playWithPcmData:(NSData *_Nonnull)data;

- (void)startRecord; //开始录音
- (void)stopRecord;  //结束录音
- (void)startPlay;   //开始放音
- (void)stopPlay;    //结束放音

- (void)stopAudioUnit;

- (void)startRecordAndPlay;  //开始通话
- (void)stopRecordAndPlay;   //结束通话

- (void)setLiveAudioSession;
- (void)setDefaultAudioSession;
- (void)setPlayingAudioSession;


@end


#import "APAudioUnitManager.h"
#import <AVFoundation/AVFoundation.h>


#define INPUT_BUS   1
#define OUTPUT_BUS  0

static AudioUnit _recordAudioUnit;
static NSMutableData *receivePcmData;
static pthread_mutex_t inputLock;
#define INPUT_LOCK pthread_mutex_lock(&inputLock);
#define INPUT_UNLOCK pthread_mutex_unlock(&inputLock);
@interface APAudioUnitManager ()

@property (assign, nonatomic) BOOL isUnitWorking;
@property (assign, nonatomic) BOOL isRecording;  //录音开关状态
@property (assign, nonatomic) BOOL isPlaying;    //放音开关状态
@property (nonatomic, strong) NSLock *lock;

@end

@implementation APAudioUnitManager


static APAudioUnitManager *sharedInstace = nil;
+ (instancetype)sharedManager {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedInstace = [[APAudioUnitManager alloc] init];
        receivePcmData = [NSMutableData data];
        [sharedInstace initRemoteIO];
    });
    return sharedInstace;
}

- (void)initRemoteIO {
    AudioUnitInitialize(_recordAudioUnit);
    _lock = [NSLock new];
    _recordFactor = 2;
    [self initBuffer];
    [self initAudioComponentWithSubType:kAudioUnitSubType_VoiceProcessingIO];
    self.sampleRate = 16000;
    [self initAudioProperty];
    [self initRecordeCallback];
    [self initPlayCallback];
}

- (void)initBuffer {
    UInt32 flag = 0;
    AudioUnitSetProperty(_recordAudioUnit,
                         kAudioUnitProperty_ShouldAllocateBuffer,
                         kAudioUnitScope_Output,
                         INPUT_BUS,
                         &flag,
                         sizeof(flag));
}

- (void)initAudioComponentWithSubType:(UInt32)subType {
    AudioComponentDescription audioDesc;
    audioDesc.componentType = kAudioUnitType_Output;
    audioDesc.componentSubType = subType;
    audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
    audioDesc.componentFlags = 0;
    audioDesc.componentFlagsMask = 0;
    
    AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioDesc);
    AudioComponentInstanceNew(inputComponent, &_recordAudioUnit);
}

- (void)setSampleRate:(int)sampleRate {
    if (sampleRate == _sampleRate) return;
    _sampleRate = sampleRate;
    [self initFormat];
}

- (void)initFormat {
    AudioStreamBasicDescription audioFormat;
    audioFormat.mSampleRate = _sampleRate;
    NSLog(@"sampleRate:%.0lf",audioFormat.mSampleRate);
    audioFormat.mFormatID = kAudioFormatLinearPCM;
    audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
    audioFormat.mFramesPerPacket = 1;
    audioFormat.mChannelsPerFrame = 1;
    audioFormat.mBitsPerChannel = 16;
    audioFormat.mBytesPerPacket = 2;
    audioFormat.mBytesPerFrame = 2;
    
    AudioUnitSetProperty(_recordAudioUnit,
                         kAudioUnitProperty_StreamFormat,
                         kAudioUnitScope_Input,
                         OUTPUT_BUS,
                         &audioFormat,
                         sizeof(audioFormat));
    AudioUnitSetProperty(_recordAudioUnit,
                         kAudioUnitProperty_StreamFormat,
                         kAudioUnitScope_Output,
                         INPUT_BUS,
                         &audioFormat,
                         sizeof(audioFormat));
}

- (void)initAudioProperty {
    UInt32 flag = 1;
    AudioUnitSetProperty(_recordAudioUnit,
                         kAudioOutputUnitProperty_EnableIO,
                         kAudioUnitScope_Input,
                         INPUT_BUS,
                         &flag,
                         sizeof(flag));
    AudioUnitSetProperty(_recordAudioUnit,
                         kAudioOutputUnitProperty_EnableIO,
                         kAudioUnitScope_Output,
                         OUTPUT_BUS,
                         &flag,
                         sizeof(flag));
    UInt32 echoCancellation = 0;
    AudioUnitSetProperty(_recordAudioUnit,
                         kAUVoiceIOProperty_BypassVoiceProcessing,
                         kAudioUnitScope_Global,
                         0,
                         &echoCancellation,
                         sizeof(echoCancellation));
}

- (void)initRecordeCallback {
    AURenderCallbackStruct recordCallback;
    recordCallback.inputProc = RecordCallback;
    recordCallback.inputProcRefCon = (__bridge void *)self;
    AudioUnitSetProperty(_recordAudioUnit,
                         kAudioOutputUnitProperty_SetInputCallback,
                         kAudioUnitScope_Output,
                         INPUT_BUS,
                         &recordCallback,
                         sizeof(recordCallback));
}

- (void)initPlayCallback {
    AURenderCallbackStruct playCallback;
    playCallback.inputProc = PlayCallback;
    playCallback.inputProcRefCon = (__bridge void *)self;
    AudioUnitSetProperty(_recordAudioUnit,
                         kAudioUnitProperty_SetRenderCallback,
                         kAudioUnitScope_Global,
                         OUTPUT_BUS,
                         &playCallback,
                         sizeof(playCallback));
}

#pragma mark - callback function

static OSStatus RecordCallback(void *inRefCon,
                               AudioUnitRenderActionFlags *ioActionFlags,
                               const AudioTimeStamp *inTimeStamp,
                               UInt32 inBusNumber,
                               UInt32 inNumberFrames,
                               AudioBufferList *ioData)
{
    APAudioUnitManager *mgr = (__bridge APAudioUnitManager *)inRefCon;
    if (mgr == nil || mgr.isUnitWorking == NO || mgr.isRecording == NO) return noErr;
    UInt16 numSamples=inNumberFrames*1;
    UInt16 samples[numSamples];
    memset (&samples, 0, sizeof (samples));
    AudioBufferList recordBufferList;
    recordBufferList.mNumberBuffers = 1;
    recordBufferList.mBuffers[0].mData = samples;
    recordBufferList.mBuffers[0].mNumberChannels = 1;
    recordBufferList.mBuffers[0].mDataByteSize = numSamples*sizeof(UInt16);
    /// 填充数据
    AudioUnitRender(_recordAudioUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, &recordBufferList);
    void *bufferData = recordBufferList.mBuffers[0].mData;
    int bufferSize = recordBufferList.mBuffers[0].mDataByteSize;
    /// 放大录音
    [mgr raiseVolume:recordBufferList.mBuffers[0].mData length:recordBufferList.mBuffers[0].mDataByteSize factor:mgr.recordFactor];
    if (mgr.recordBufferCallback) {
        mgr.recordBufferCallback(bufferData, bufferSize);
    }
    if (mgr.volumeCallback) {
        mgr.volumeCallback(dBValueFromPCM(bufferData, bufferSize));
    }
    
    return noErr;
}

- (void)raiseVolume:(unsigned char *)buffer length:(UInt32)length factor:(int)factor{
    if (_recordFactor <= 1) return;
    signed short MIN = -0x8000;
    signed short MAX = 0x7FFF;
    signed short low = 0, high = 0, data = 0, maxData = 0, minData = 0;
    //获取一个音频帧中的最大值`max`和最小值`min`
    for (int i = 0; i < length; i+=2) {
        low = buffer[i];
        high = buffer[i+1];
        data = low+(high<<8);
        maxData = maxData > data ? maxData : data;
        minData = minData < data ? minData : data;
    }
    //根据获取到的最大值和最小值分别计算出在不失真的情况下，允许的放大倍数`maxfactor`和`minfactor`
    signed short maxfactor =  maxData != 0 ? MAX/maxData : 1;
    signed short minfactor =  minData != 0 ? MIN/minData : 1;
    if (minfactor == 1 || maxfactor == 1) {
        return;
    }
    //取其最小值为允许的放大倍数`allowfactor`
    signed short allowfactor = MIN(maxfactor, minfactor);
    //根据经验系数`factor`，选择合适的系数
    factor = factor > allowfactor ? allowfactor : factor;
    if (factor <= 1) {
        return;
    }
    //对PCM数据放大
    signed long newData = 0;
    for (int i = 0; i < length; i+=2) {
        low = buffer[i];
        high = buffer[i+1];
        data = low+(high<<8);
        newData = data*factor;
        //边界值溢出处理
        if (newData < MIN) {
            newData = MIN;
        }else if (newData > MAX) {
            newData = MAX;
        }
        data = newData&0xffff;
        buffer[i] = data&0x00ff;
        buffer[i+1] = (data&0xff00)>>8;
    }
}

static float dBValueFromPCM(const char *data,int length)  {
    /// 采样位深为16bit,即两个字节 ,因此每两个字节读取振幅值，然后取平均值
    short int amp = 0;
    float ampSum = 0;
    for (int i = 0; i < length; i += 2) {
        memcpy(&amp, data+i, 2);
        ampSum += abs(amp);
    }
    /// 平均振幅值
    float ampAve = ampSum / (length / 2);
    /// 振幅和分贝的计算表达式
    return log10(ampAve)*20.0;
}

static OSStatus PlayCallback(void *inRefCon,
                             AudioUnitRenderActionFlags *ioActionFlags,
                             const AudioTimeStamp *inTimeStamp,
                             UInt32 inBusNumber,
                             UInt32 inNumberFrames,
                             AudioBufferList *ioData) {
    APAudioUnitManager *mgr = (__bridge APAudioUnitManager *)inRefCon;
    if (mgr == nil || mgr.isUnitWorking == NO) return noErr;
    INPUT_LOCK
    UInt32 bufferSize = ioData->mBuffers[0].mDataByteSize;
    if (receivePcmData.length >= bufferSize) {
        AudioBuffer aBuffer = ioData->mBuffers[0];
        memcpy(aBuffer.mData, receivePcmData.bytes, bufferSize);
        aBuffer.mDataByteSize = bufferSize;
        /// 删除已拷贝的数据
        [receivePcmData replaceBytesInRange:NSMakeRange(0, bufferSize) withBytes:NULL length:0];
    }else {
        for (UInt32 i=0; i < ioData->mNumberBuffers; i++)
        {
            memset(ioData->mBuffers[i].mData, 0, ioData->mBuffers[i].mDataByteSize);
        }
    }
    INPUT_UNLOCK
    return noErr;
}

#pragma mark - public methods

- (void)setEchoCancellationEnabled:(BOOL)echoCancellationEnabled {
    UInt32 cancelEcho = echoCancellationEnabled ? 0 : 1;
    UInt32 status;
    UInt32 statusSize = sizeof(status);
    AudioUnitGetProperty(_recordAudioUnit, kAUVoiceIOProperty_BypassVoiceProcessing, kAudioUnitScope_Global, 0, &status, &statusSize);
    if (status != cancelEcho) {
        AudioUnitSetProperty(_recordAudioUnit, kAUVoiceIOProperty_BypassVoiceProcessing, kAudioUnitScope_Global, 0, &cancelEcho, sizeof(cancelEcho));
        _echoCancellationEnabled = cancelEcho == 0;
    }
}

- (void)playWithPcmData:(NSData *)data {
    if (!self.isPlaying) return;
    INPUT_LOCK
    [receivePcmData appendData:data];
    INPUT_UNLOCK
}

- (void)startRecord {
    NSLog(@"开始录音");
    [self startAudioUnit];
    self.isRecording = YES;
    
}

- (void)stopRecord {
    NSLog(@"暂停录音");
    self.isRecording = NO;
}

- (void)startPlay {
    [self startAudioUnit];
    self.isPlaying = YES;
}

- (void)stopPlay {
    if (self.isPlaying) {
        resetBuffer();
        self.isPlaying = NO;
    }
}

- (void)startAudioUnit {
    if (self.isUnitWorking) return;
    self.isUnitWorking = YES;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [self setLiveAudioSession];
        AudioOutputUnitStart(_recordAudioUnit);
    });
}

- (void)stopAudioUnit {
    if (!self.isUnitWorking) return;
    self.isUnitWorking = NO;
    [self stopPlay];
    [self stopRecord];
    receivePcmData.length = 0;
    AudioOutputUnitStop(_recordAudioUnit);
    [self setDefaultAudioSession];
}



- (void)setLiveAudioSession {
    NSError * error = nil;
    AVAudioSession * sessionInstance = [AVAudioSession sharedInstance];
    [sessionInstance setActive:NO error:&error];
    if (error) NSLog(@"audio session error21:%@",error);
    AVAudioSessionCategoryOptions options = AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowBluetooth;
    if (@available(iOS 10.0, *)) {
        options |= AVAudioSessionCategoryOptionAllowBluetoothA2DP;
    }
    [sessionInstance setCategory:AVAudioSessionCategoryPlayAndRecord
                     withOptions:options
                           error:&error];
    if (error) NSLog(@"audio session error22:%@",error);
    [sessionInstance setActive:YES error:&error];
    if (error) NSLog(@"audio session error23:%@",error);
}

- (void)setDefaultAudioSession {
    NSError * error = nil;
    AVAudioSession * sessionInstance = [AVAudioSession sharedInstance];
    [sessionInstance setActive:NO error:&error];
    if (error) NSLog(@"audio session error11:%@",error);
    AVAudioSessionCategoryOptions options = AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowBluetooth;
    if (@available(iOS 10.0, *)) {
        options |= AVAudioSessionCategoryOptionAllowBluetoothA2DP;
    }
    [sessionInstance setCategory:AVAudioSessionCategorySoloAmbient withOptions:options error:&error];
    if (error) NSLog(@"audio session error12:%@",error);
    [sessionInstance setActive:YES error:&error];
    if (error) NSLog(@"audio session error13:%@",error);
}

- (void)setPlayingAudioSession {
    NSError * error = nil;
    AVAudioSession * sessionInstance = [AVAudioSession sharedInstance];
    [sessionInstance setActive:NO error:&error];
    if (error) NSLog(@"audio session error31:%@",error);
    AVAudioSessionCategoryOptions options = AVAudioSessionCategoryOptionDefaultToSpeaker | AVAudioSessionCategoryOptionAllowBluetooth;
    if (@available(iOS 10.0, *)) {
        options |= AVAudioSessionCategoryOptionAllowBluetoothA2DP;
    }
    [sessionInstance setCategory:AVAudioSessionCategoryPlayback withOptions:options error:&error];
    if (error) NSLog(@"audio session error32:%@",error);
    [sessionInstance setActive:YES error:&error];
    if (error) NSLog(@"audio session error33:%@",error);
}

NS_INLINE void resetBuffer(void) {
    INPUT_LOCK
    [receivePcmData replaceBytesInRange:NSMakeRange(0, receivePcmData.length) withBytes:NULL length:0];
    INPUT_UNLOCK
}//
//  APG711AudioCodec.h
//  APP2PDemo
//
//  Created by michael on 2023/3/28.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN


@interface APG711AudioCodec : NSObject

- (NSData *)encodeG711Data:(NSData *)data isUlaw:(BOOL)isUlaw;

- (NSData *)decodeG711Data:(NSData *)data isUlaw:(BOOL)isUlaw;


@end

NS_ASSUME_NONNULL_END

//
//  APG711AudioCodec.m
//  APP2PDemo
//
//  Created by michael on 2023/3/28.
//

#import "APG711AudioCodec.h"
#include "lsg711core.h"

@implementation APG711AudioCodec

- (NSData *)decodeG711Data:(NSData *)data isUlaw:(BOOL)isUlaw {
    uint8_t *decodeBuffer = (uint8_t *)malloc(data.length*2);
    LSG711Decoder((short *)decodeBuffer, (unsigned char*)data.bytes, (int)data.length, isUlaw);
    NSData *decodeData = [NSData dataWithBytes:decodeBuffer length:data.length*2];
    free(decodeBuffer);
    return decodeData;
}

- (NSData *)encodeG711Data:(NSData *)data isUlaw:(BOOL)isUlaw {
    char *encodeBuffer = (char *)malloc(data.length/2);
    LSG711Encoder((short *)data.bytes, (unsigned char*)encodeBuffer, (int)data.length / 2, isUlaw);
    NSData *encodeData = [NSData dataWithBytes:encodeBuffer length:data.length/2];
    free(encodeBuffer);
    return encodeData;
}


@end




- (void)audio_release {
    AudioUnitUninitialize(_recordAudioUnit);
}


@end

//
//  APAACEncoder.h
//  arpha plus
//
//  Created by michael on 2023/4/4.
//

#import <Foundation/Foundation.h>
#import <CoreMedia/CMSampleBuffer.h>

NS_ASSUME_NONNULL_BEGIN

@interface ATBAudioConfig : NSObject

/// 采样率
@property (nonatomic, assign) NSInteger sampleRate;

///通道数
@property (nonatomic, assign) NSInteger channelCount;

///采样位深
@property (nonatomic, assign) NSInteger sampleBits;

+(instancetype)defaulConfig;

@end

@protocol APAACEncoderDelegate <NSObject>
- (void)audioEncodeCallback:(NSData *)aacData;
@end

@interface APAACEncoder : NSObject

@property (nonatomic, strong) ATBAudioConfig *audioConfig;
@property (nonatomic, weak) id<APAACEncoderDelegate> delegate;

- (void)encodeAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer;

- (void)encodeAudioData:(NSData *)pcmData;

- (void)clear;

- (NSData *)ADTSHeaderWithLength:(int)data_length;

@end

NS_ASSUME_NONNULL_END

//
//  APAACEncoder.m
//  arpha plus
//
//  Created by michael on 2023/4/4.
//

#import "APAACEncoder.h"
#import <AudioToolBox/AudioToolBox.h>

@implementation ATBAudioConfig

- (instancetype)init
{
    self = [super init];
    if (self) {
        _channelCount = 1;
        _sampleRate = 8000;
        _sampleBits = 16;
    }
    return self;
}

+ (instancetype)defaulConfig {
    return [[ATBAudioConfig alloc] init];
}


@end

#define BytesPerPacket 2
#define AACFramePerPacket 1024
@interface APAACEncoder ()
@property (nonatomic) dispatch_queue_t encodeQueue;
@property (nonatomic) dispatch_queue_t callbackQueue;
@property (nonatomic) AudioConverterRef audioConverter;
@property (nonatomic, strong) NSMutableData *pcmData;

@end

@implementation APAACEncoder

- (NSMutableData *)pcmData {
    if (!_pcmData) {
        _pcmData = [NSMutableData data];
    }
    return _pcmData;
}


- (instancetype)init
{
    self = [super init];
    if (self) {
        _encodeQueue = dispatch_queue_create("aac data encode queue", DISPATCH_QUEUE_SERIAL);
        _callbackQueue = dispatch_queue_create("aac data callback queue", DISPATCH_QUEUE_SERIAL);
        //音频转换器
        _audioConfig = [ATBAudioConfig defaulConfig];
        [self initConverter];
    }
    return self;
}


static OSStatus aacEncodeInputDataProc(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData){
    AudioBufferList bufferList = *(AudioBufferList *)inUserData;
    ioData->mNumberBuffers = bufferList.mNumberBuffers;
    ioData->mBuffers[0].mNumberChannels = bufferList.mBuffers->mNumberChannels;
    ioData->mBuffers[0].mData = bufferList.mBuffers[0].mData;
    ioData->mBuffers[0].mDataByteSize = bufferList.mBuffers[0].mDataByteSize;
    return noErr;
}


- (void)encodeAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer  {
    [self encodeAudioData:[self audioDataWithSampleBuffer:sampleBuffer]];
}

- (NSData *)audioDataWithSampleBuffer:(CMSampleBufferRef )sampleBuffer {
    if (sampleBuffer) {
        CFRetain(sampleBuffer);
        CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
        if (blockBuffer) {
            size_t  pcmLength = 0;
            char *pcmBuffer;
            OSStatus status = CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, &pcmLength, &pcmBuffer);
            if (status != kCMBlockBufferNoErr) {
                CFRelease(sampleBuffer);
                 NSLog(@"Get ACC from blockBuffer error \n");
                return nil;
            }else {
                NSData *pcmData = [NSData dataWithBytes:pcmBuffer length:pcmLength];
                CFRelease(sampleBuffer);
                return pcmData;
            }
        }else {
            CFRelease(sampleBuffer);
            return nil;
        }
    }
    return nil;
}

- (void)clear {
    [self.pcmData replaceBytesInRange:NSMakeRange(0, self.pcmData.length) withBytes:NULL length:0];
}


- (void)encodeAudioData:(NSData *)pcmData{
    if ([pcmData length] == 0) return;
    dispatch_async(self.encodeQueue, ^{
        size_t maxBufferSize = BytesPerPacket * AACFramePerPacket * self.audioConfig.channelCount;
        [self.pcmData appendData:pcmData];
        if (self.pcmData.length >= maxBufferSize) {
            NSMutableData *aacData = [NSMutableData data];
            NSUInteger count = self.pcmData.length / maxBufferSize;
            /// 注：此处之所加循环是为了防止，传过来的buffer非常大，比如30000多的数据，而，编码器每次只能处理`maxBufferSize`长度的数据.
            /// 实际情况，输入pcm的长度一般不会很大
            for (int i = 0; i < count; i++) {
                uint8_t *aacBuffer = (uint8_t *)calloc(1, maxBufferSize);
                AudioBufferList inputBufferlist ;
                inputBufferlist.mNumberBuffers = 1;
                inputBufferlist.mBuffers ->mNumberChannels = (UInt32) self.audioConfig.channelCount;
                inputBufferlist.mBuffers->mDataByteSize = (UInt32) maxBufferSize;
                inputBufferlist.mBuffers->mData = (void *)self.pcmData.bytes;
                
                AudioBufferList outputBufferlist ;
                outputBufferlist.mNumberBuffers = 1;
                outputBufferlist.mBuffers ->mNumberChannels = inputBufferlist.mBuffers->mNumberChannels;
                outputBufferlist.mBuffers->mDataByteSize = (UInt32) maxBufferSize;
                outputBufferlist.mBuffers->mData = aacBuffer;
                UInt32 outputNumPackets = 1;
               /// 编码器填充数据
                OSStatus status = AudioConverterFillComplexBuffer(self.audioConverter, aacEncodeInputDataProc, &inputBufferlist, &outputNumPackets, &outputBufferlist, NULL);
                if (status != noErr) {
                    NSLog(@"audio converter fillComplexBuffer error %d",(int)status);
                }
                [aacData appendBytes:outputBufferlist.mBuffers[0].mData length:outputBufferlist.mBuffers[0].mDataByteSize];
                ///每次循环去除前面的`maxBufferSize`长度的数据
                if (self.pcmData.length >= maxBufferSize) {
                    [self.pcmData replaceBytesInRange:NSMakeRange(0, maxBufferSize) withBytes:NULL length:0];
                }
                free(aacBuffer);
            }
            NSLog(@"audio aac length:%d",aacData.length);
            [self delegateWithAACData:aacData];
        }
    });
}


- (void)delegateWithAACData:(NSData *)aacData {
    if ([self.delegate respondsToSelector:@selector(audioEncodeCallback:)]) {
        [self.delegate audioEncodeCallback:aacData];
    }
}


-(void)initConverter {
    //设置输入源 PCM 的音频参数
    AudioStreamBasicDescription inputAudioDes = {0};
    inputAudioDes.mSampleRate = self.audioConfig.sampleRate;
    inputAudioDes.mFormatID = kAudioFormatLinearPCM;
    inputAudioDes.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
    inputAudioDes.mChannelsPerFrame = (uint32_t)self.audioConfig.channelCount;
    inputAudioDes.mBitsPerChannel = (uint32_t)self.audioConfig.sampleBits;
    inputAudioDes.mFramesPerPacket = 1;
    inputAudioDes.mBytesPerFrame = inputAudioDes.mBitsPerChannel / 8 * inputAudioDes.mChannelsPerFrame;
    inputAudioDes.mBytesPerPacket = inputAudioDes.mBytesPerFrame * inputAudioDes.mFramesPerPacket;;
    
    //设置输出AAC 的编码参数
    AudioStreamBasicDescription outputAudioDes = {0};
    outputAudioDes.mFormatID = kAudioFormatMPEG4AAC;
    outputAudioDes.mFormatFlags = kMPEG4Object_AAC_LC;
    outputAudioDes.mSampleRate = self.audioConfig.sampleRate;
    outputAudioDes.mChannelsPerFrame = (uint32_t)self.audioConfig.channelCount;  ///声道数
    outputAudioDes.mFramesPerPacket = 1024;///每个packet 的帧数 ，这是一个比较大的固定数值
    outputAudioDes.mBytesPerFrame = 0; //每帧的大小  如果是压缩格式设置为0
    outputAudioDes.mReserved = 0; // 8字节对齐，填0;
    
    OSStatus status = noErr;
    if (1) {
        AudioClassDescription *description = [self
                                              getAudioClassDescriptionWithType:kAudioFormatMPEG4AAC
                                              fromManufacturer:kAppleSoftwareAudioCodecManufacturer];
        
        status = AudioConverterNewSpecific(&inputAudioDes, &outputAudioDes, 1, description, &_audioConverter);
    }else {
        uint32_t outDesSize = sizeof(outputAudioDes);
        AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &outDesSize, &outputAudioDes);
        status = AudioConverterNew(&inputAudioDes, &outputAudioDes, &_audioConverter);
    }
    if (status != noErr) {
        NSLog(@"硬编码AAC创建失败");
    }
}

- (AudioClassDescription *)getAudioClassDescriptionWithType:(UInt32)type
                                           fromManufacturer:(UInt32)manufacturer
{
    static AudioClassDescription desc;
    
    UInt32 encoderSpecifier = type;
    OSStatus st;
    
    UInt32 size;
    st = AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders,
                                    sizeof(encoderSpecifier),
                                    &encoderSpecifier,
                                    &size);
    if (st) {
        return nil;
    }
    
    unsigned int count = size / sizeof(AudioClassDescription);
    AudioClassDescription descriptions[count];
    st = AudioFormatGetProperty(kAudioFormatProperty_Encoders,
                                sizeof(encoderSpecifier),
                                &encoderSpecifier,
                                &size,
                                descriptions);
    if (st) {
        return nil;
    }
    
    for (unsigned int i = 0; i < count; i++) {
        if ((type == descriptions[i].mSubType) &&
            (manufacturer == descriptions[i].mManufacturer)) {
            memcpy(&desc, &(descriptions[i]), sizeof(desc));
            return &desc;
        }
    }
    
    return nil;
}




- (NSData *)ADTSHeaderWithLength:(int)data_length {
    NSInteger profile = self.audioConfig.sampleBits/8;
    return  [self adtsHeaderWithLength:data_length profile:profile sampleRate:self.audioConfig.sampleRate channles:self.audioConfig.channelCount];
}



- (NSData *)adtsHeaderWithLength:(int)data_length profile:(int)profile sampleRate:(int)sampleRate channles:(int)channles {
    
    int adtsLength = 7;
    profile = 2;  /// AAC LC
    int chanCfg = 1;
    char *adts_header = malloc(sizeof(char) * adtsLength);
    int fullLength = adtsLength + data_length;
    int freqIdx = [self fregWithSampleBuffer:sampleRate];    //对应44100采样率；
    /*
    A 12 syncword 0xFFF, all bits must be 1
    //  11111111
    */
    adts_header[0] = 0xFF;
    /*
    B 1 MPEG Version: 0 for MPEG-4, 1 for MPEG-2
    C 2 Layer: always 0
    D 1 protection absent, Warning, set to 1 if there is no CRC and 0 if there is CRC
    ///  1111 1001
    */
    adts_header[1] = 0xF9;
    /*
    E 2 profile, the MPEG-4 Audio Object Type minus 1
    F 4 MPEG-4 Sampling Frequency Index (15 is forbidden)
    G 1 private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding
    H 3 MPEG-4 Channel Configuration (in the case of 0, the channel configuration is sent via an inband
     11
    */
    adts_header[2] = (char)(((profile-1) << 6));
    adts_header[2] |= (char)(freqIdx << 2);
    adts_header[2] |= (char)(chanCfg >> 2);
    
    /*
      前两位已经被H占了
     I 1 originality, set to 0 when encoding, ignore when decoding
     J 1 home, set to 0 when encoding, ignore when decoding
     K 1 copyrighted id bit, the next bit of a centrally registered copyright identifier, set to 0 when encoding, ignore when decoding
     L 1 copyright id start, signals that this frame's copyright id bit is the first bit of the copyright id, set to 0 when encoding, ignore when decoding

     xx0000xx
     */
    adts_header[3] = (char)((chanCfg & 3) <<6); //chanCfg 的2bit
    
    /*
     M 13 frame length, this value must include 7 or 9 bytes of header length: FrameLength = (ProtectionAbsent == 1 ? 7 : 9) + size(AACFrame)
     0x7FF = 11111111111
     */
    adts_header[3]  |= (char)((fullLength & 0x18) >> 11);//这里只占了2bit 所以，13bit 又移11位
    adts_header[4] = (char)((fullLength &0x7FF) >> 3);
   
    //前3bit 是fulllength 的低位
    adts_header[5] =  (char)((fullLength & 7) << 5);
    /*
     O 11 Buffer fullness
     */
    adts_header[5] |= 0x1f;
    /*
     Q 16 CRC if protection absent is 0
     */
    adts_header[6] = (char)0xFC;
    
    NSData *data = [[NSData alloc] initWithBytes:adts_header length:adtsLength];
    return data;
}

- (int)fregWithSampleBuffer:(NSUInteger)sampelBuffer {
    switch (sampelBuffer) {
        case 7350: return 0xc;
        case 8000: return 0xb;
        case 11025: return 0xa;
        case 12000: return 0x9;
        case 16000: return 0x8;
        case 22050: return 0x7;
        case 24000: return 0x6;
        case 32000: return 0x5;
        case 44100: return 0x4;
        case 48000: return 0x3;
        case 64000: return 0x2;
        case 88200: return 0x1;
        case 96000: return 0x0;
        default: return 4;
    }
}

- (void)dealloc {
    AudioConverterDispose(_audioConverter);
    _audioConverter = nil;
}

@end

//
//  APAudioCodec.h
//  FuweiCloud
//
//  Created by michael on 2023/12/25.
//

#import <Foundation/Foundation.h>
#import <AudioToolBox/AudioToolBox.h>

NS_ASSUME_NONNULL_BEGIN

@class APAudioCodec;
@protocol APAudioCodecDelegate <NSObject>

- (void)codec:(APAudioCodec *)codec outputData:(NSData *)data;

@end

@interface APAudioCodec : NSObject

@property (nonatomic, assign, readonly) BOOL isEncoder;

+ (instancetype)codecWithFormatID:(AudioFormatID)formatID isEncoder:(BOOL)isEncoder delegate:(id<APAudioCodecDelegate>)delegate;

- (void)inputData:(NSData *)data;

@end

NS_ASSUME_NONNULL_END

//
//  APAudioCodec.m
//  FuweiCloud
//
//  Created by michael on 2023/12/25.
//

#import "APAudioCodec.h"
#import "APSampleBufferWrapper.h"

#define SAMPLE_RATE 8000
#define MAX_PACKET_SIZE 2048
@interface APAudioCodec ()
@property (nonatomic, weak) id<APAudioCodecDelegate> delegate;
@property (nonatomic, assign) AudioFormatID formatID;
@property (nonatomic, assign) AudioConverterRef converter;
@property (nonatomic, assign) BOOL isEncoder;
@property (nonatomic, assign) BOOL isAAC;
@property (nonatomic, strong) NSMutableData *inputData;
@property (nonatomic, strong) dispatch_queue_t codecQueue;
@property (nonatomic, assign) NSUInteger framePerPacket;
@property (nonatomic, assign) uint8_t *outputBuffer;

@end

@implementation APAudioCodec

- (NSUInteger)framePerPacket {
    switch (self.formatID) {
        case kAudioFormatMPEG4AAC: return 1024;
        case kAudioFormatULaw:
        case kAudioFormatALaw:
            return 160;
        case kAudioFormatMPEGLayer3:
            return 1152;
        default: return 1;
    }
}

- (BOOL)isAAC {
    return self.formatID == kAudioFormatMPEG4AAC;;
}

+ (instancetype)codecWithFormatID:(AudioFormatID)formatID isEncoder:(BOOL)isEncoder  delegate:(id<APAudioCodecDelegate>)delegate {
    return [[self alloc] initWithFormatID:formatID isEncoder:isEncoder delegate:delegate];
}

- (instancetype)initWithFormatID:(AudioFormatID)formatID isEncoder:(BOOL)isEncoder delegate:(id<APAudioCodecDelegate>)delegate {
    if (self = [super init]) {
        self.outputBuffer = (uint8_t *)calloc(1, MAX_PACKET_SIZE);
        self.delegate = delegate;
        self.formatID = formatID;
        self.isEncoder = isEncoder;
        self.codecQueue = dispatch_queue_create("audio codec input queue", DISPATCH_QUEUE_SERIAL);
        self.inputData = [NSMutableData data];
        BOOL isAAC = self.isAAC;
        var inASBD = ASBD(isEncoder?kAudioFormatLinearPCM:formatID, isEncoder?12:(isAAC?2:0), SAMPLE_RATE, 1, isAAC?1024:1);
        var outASBD = ASBD(isEncoder?formatID:kAudioFormatLinearPCM, isEncoder?(isAAC?2:0):12, SAMPLE_RATE, 1, isAAC?1024:1);
        AudioClassDescription *desc = [self getAudioClassDescription];
        if (desc) {
            AudioConverterNewSpecific(&inASBD, &outASBD, 1, desc, &_converter);
            if (NULL == _converter) {
                NSLog(@"init converter error");
            }else {
                
            }
        }else {
            NSLog(@"getAudioClassDescription error");
        }
    }
    return self;
}

- (AudioClassDescription *)getAudioClassDescription
{
    static AudioClassDescription desc;
    UInt32 encoderSpecifier = self.formatID;
    OSStatus st;
    AudioFormatPropertyID proID = self.isEncoder?kAudioFormatProperty_Encoders:kAudioFormatProperty_Decoders;
    UInt32 size;
    st = AudioFormatGetPropertyInfo(proID,
                                    sizeof(encoderSpecifier),
                                    &encoderSpecifier,
                                    &size);
    if (st) return nil;
    
    unsigned int count = size / sizeof(AudioClassDescription);
    AudioClassDescription descriptions[count];
    st = AudioFormatGetProperty(proID,
                                sizeof(encoderSpecifier),
                                &encoderSpecifier,
                                &size,
                                descriptions);
    if (st) return nil;
    
    for (unsigned int i = 0; i < count; i++) {
        if ((self.formatID == descriptions[i].mSubType) &&
            (kAppleSoftwareAudioCodecManufacturer == descriptions[i].mManufacturer)) {
            memcpy(&desc, &(descriptions[i]), sizeof(desc));
            return &desc;
        }
    }
    return nil;
}

- (void)inputData:(NSData *)data {
    if (NULL == _converter) {
        NSLog(@"converter is nil");
        return;
    }
    dispatch_async(self.codecQueue, ^{
        size_t rawBufferSize = self.framePerPacket;
        size_t maxBufferSize = 2 * rawBufferSize; ///< 估算aac和g711的压缩比为2:1
        size_t packetByteSize = self.isEncoder?maxBufferSize:rawBufferSize;
        [self.inputData appendData:data];
        if (self.inputData.length >= packetByteSize) {
            NSData *outputData = nil;
            NSUInteger count = self.inputData.length / maxBufferSize;
            /// 注：此处之所加循环是为了防止，传过来的buffer非常大，比如30000多的数据，而，编码器每次只能处理`maxBufferSize`长度的数据.
            /// 实际情况，输入pcm的长度一般不会很大
            for (int i = 0; i < count; i++) {
                AudioBufferList inputBufferlist ;
                inputBufferlist.mNumberBuffers = 1;
                inputBufferlist.mBuffers ->mNumberChannels = 1;
                inputBufferlist.mBuffers->mDataByteSize = (UInt32)packetByteSize;
                inputBufferlist.mBuffers->mData = (void *)self.inputData.bytes;
                
                ///填充数据
                AudioBufferList outputBufferlist ;
                outputBufferlist.mNumberBuffers = 1;
                outputBufferlist.mBuffers ->mNumberChannels = 1;
                outputBufferlist.mBuffers->mDataByteSize = (UInt32)maxBufferSize;
                outputBufferlist.mBuffers->mData = self.outputBuffer;
                UInt32 outputNumPackets = (UInt32)self.framePerPacket;
               /// 编码器填充数据
                OSStatus status = AudioConverterFillComplexBuffer(self.converter, ioDataProc, &inputBufferlist, &outputNumPackets, &outputBufferlist, NULL);
                if (status != noErr) {
                    NSLog(@"audio converter fillComplexBuffer error %d",(int)status);
                }
                outputData = [NSData dataWithBytes:outputBufferlist.mBuffers[0].mData length:outputBufferlist.mBuffers[0].mDataByteSize];
                ///每次循环去除前面的`maxBufferSize`长度的数据
                if (self.inputData.length >= packetByteSize) {
                    [self.inputData replaceBytesInRange:NSMakeRange(0, packetByteSize) withBytes:NULL length:0];
                }
            }
            if ([self.delegate respondsToSelector:@selector(codec:outputData:)]) {
                [self.delegate codec:self outputData:outputData];
            }
        }
    });
}

- (void)dealloc {
    if (_outputBuffer) {
        free(_outputBuffer);
        _outputBuffer = NULL;
    }
}

static OSStatus ioDataProc(AudioConverterRef inAudioConverter, UInt32 *ioNumberDataPackets, AudioBufferList *ioData, AudioStreamPacketDescription **outDataPacketDescription, void *inUserData){
    AudioBufferList bufferList = *(AudioBufferList *)inUserData;
    ioData->mNumberBuffers = bufferList.mNumberBuffers;
    ioData->mBuffers[0].mNumberChannels = bufferList.mBuffers->mNumberChannels;
    ioData->mBuffers[0].mData = bufferList.mBuffers[0].mData;
    ioData->mBuffers[0].mDataByteSize = bufferList.mBuffers[0].mDataByteSize;
    return noErr;
}

@end

/*
 * This source code is a product of Sun Microsystems, Inc. and is provided
 * for unrestricted use.  Users may copy or modify this source code without
 * charge.
 *
 * SUN SOURCE CODE IS PROVIDED AS IS WITH NO WARRANTIES OF ANY KIND INCLUDING
 * THE WARRANTIES OF DESIGN, MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE OR TRADE PRACTICE.
 *
 * Sun source code is provided with no support and without any obligation on
 * the part of Sun Microsystems, Inc. to assist in its use, correction,
 * modification or enhancement.
 *
 * SUN MICROSYSTEMS, INC. SHALL HAVE NO LIABILITY WITH RESPECT TO THE
 * INFRINGEMENT OF COPYRIGHTS, TRADE SECRETS OR ANY PATENTS BY THIS SOFTWARE
 * OR ANY PART THEREOF.
 *
 * In no event will Sun Microsystems, Inc. be liable for any lost revenue
 * or profits or other special, indirect and consequential damages, even if
 * Sun has been advised of the possibility of such damages.
 *
 * Sun Microsystems, Inc.
 * 2550 Garcia Avenue
 * Mountain View, California  94043
 */

/*
 * g711.h
 *
 * u-law, A-law and linear PCM conversions.
 */

#ifndef G711_H
#define G711_H
#ifdef __cplusplus
extern "C" {
#endif
/*
 * linear2alaw() - Convert a 16-bit linear PCM value to 8-bit A-law
 *
 * linear2alaw() accepts an 16-bit integer and encodes it as A-law data.
 *
 *        Linear Input Code    Compressed Code
 *    ------------------------    ---------------
 *    0000000wxyza            000wxyz
 *    0000001wxyza            001wxyz
 *    000001wxyzab            010wxyz
 *    00001wxyzabc            011wxyz
 *    0001wxyzabcd            100wxyz
 *    001wxyzabcde            101wxyz
 *    01wxyzabcdef            110wxyz
 *    1wxyzabcdefg            111wxyz
 *
 * For further information see John C. Bellamy's Digital Telephony, 1982,
 * John Wiley & Sons, pps 98-111 and 472-476.
 */

/* pcm_val is 2's complement (16-bit range) */
unsigned char ls_af_linear2alaw (short pcm_val);

/*
 * alaw2linear() - Convert an A-law value to 16-bit linear PCM
 *
 */

short ls_af_alaw2linear (unsigned char a_val);

/*
 * linear2ulaw() - Convert a linear PCM value to u-law
 *
 * In order to simplify the encoding process, the original linear magnitude
 * is biased by adding 33 which shifts the encoding range from (0 - 8158) to
 * (33 - 8191). The result can be seen in the following encoding table:
 *
 *    Biased Linear Input Code    Compressed Code
 *    ------------------------    ---------------
 *    00000001wxyza            000wxyz
 *    0000001wxyzab            001wxyz
 *    000001wxyzabc            010wxyz
 *    00001wxyzabcd            011wxyz
 *    0001wxyzabcde            100wxyz
 *    001wxyzabcdef            101wxyz
 *    01wxyzabcdefg            110wxyz
 *    1wxyzabcdefgh            111wxyz
 *
 * Each biased linear code has a leading 1 which identifies the segment
 * number. The value of the segment number is equal to 7 minus the number
 * of leading 0's. The quantization interval is directly available as the
 * four bits wxyz.  * The trailing bits (a - h) are ignored.
 *
 * Ordinarily the complement of the resulting code word is used for
 * transmission, and so the code word is complemented before it is returned.
 *
 * For further information see John C. Bellamy's Digital Telephony, 1982,
 * John Wiley & Sons, pps 98-111 and 472-476.
 */

/* pcm_val is 2's complement (16-bit range) */
unsigned char ls_af_linear2ulaw (short pcm_val);

/*
 * ulaw2linear() - Convert a u-law value to 16-bit linear PCM
 *
 * First, a biased linear code is derived from the code word. An unbiased
 * output can then be obtained by subtracting 33 from the biased code.
 *
 * Note that this function expects to be passed the complement of the
 * original code word. This is in keeping with ISDN conventions.
 */

short ls_af_ulaw2linear (unsigned char u_val);

void LSG711Encoder(short *pcm,unsigned char *code,int size,int lawflag);
void LSG711Decoder(short *pcm,unsigned char *code,int size,int lawflag);

#ifdef __cplusplus
}
#endif

#endif /* G711_H */

/*
 * This source code is a product of Sun Microsystems, Inc. and is provided
 * for unrestricted use.  Users may copy or modify this source code without
 * charge.
 *
 * SUN SOURCE CODE IS PROVIDED AS IS WITH NO WARRANTIES OF ANY KIND INCLUDING
 * THE WARRANTIES OF DESIGN, MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE, OR ARISING FROM A COURSE OF DEALING, USAGE OR TRADE PRACTICE.
 *
 * Sun source code is provided with no support and without any obligation on
 * the part of Sun Microsystems, Inc. to assist in its use, correction,
 * modification or enhancement.
 *
 * SUN MICROSYSTEMS, INC. SHALL HAVE NO LIABILITY WITH RESPECT TO THE
 * INFRINGEMENT OF COPYRIGHTS, TRADE SECRETS OR ANY PATENTS BY THIS SOFTWARE
 * OR ANY PART THEREOF.
 *
 * In no event will Sun Microsystems, Inc. be liable for any lost revenue
 * or profits or other special, indirect and consequential damages, even if
 * Sun has been advised of the possibility of such damages.
 *
 * Sun Microsystems, Inc.
 * 2550 Garcia Avenue
 * Mountain View, California  94043
 */
#include "lsg711core.h"

#define SUPERCEDED

/*
 * g711.c
 *
 * u-law, A-law and linear PCM conversions.
 */
#define    LS_SIGN_BIT    (0x80)        /* Sign bit for a A-law byte. */
#define    LS_QUANT_MASK    (0xf)        /* Quantization field mask. */
#define    LS_NSEGS        (8)        /* Number of A-law segments. */
#define    LS_SEG_SHIFT    (4)        /* Left shift for segment number. */
#define    LS_SEG_MASK    (0x70)        /* Segment field mask. */

/* copy from CCITT G.711 specifications */
unsigned char ls_u2a[128] = {            /* u- to A-law conversions */
    1,    1,    2,    2,    3,    3,    4,    4,
    5,    5,    6,    6,    7,    7,    8,    8,
    9,    10,    11,    12,    13,    14,    15,    16,
    17,    18,    19,    20,    21,    22,    23,    24,
    25,    27,    29,    31,    33,    34,    35,    36,
    37,    38,    39,    40,    41,    42,    43,    44,
    46,    48,    49,    50,    51,    52,    53,    54,
    55,    56,    57,    58,    59,    60,    61,    62,
    64,    65,    66,    67,    68,    69,    70,    71,
    72,    73,    74,    75,    76,    77,    78,    79,
    81,    82,    83,    84,    85,    86,    87,    88,
    89,    90,    91,    92,    93,    94,    95,    96,
    97,    98,    99,    100,    101,    102,    103,    104,
    105,    106,    107,    108,    109,    110,    111,    112,
    113,    114,    115,    116,    117,    118,    119,    120,
    121,    122,    123,    124,    125,    126,    127,    128};

unsigned char ls_a2u[128] = {            /* A- to u-law conversions */
    1,    3,    5,    7,    9,    11,    13,    15,
    16,    17,    18,    19,    20,    21,    22,    23,
    24,    25,    26,    27,    28,    29,    30,    31,
    32,    32,    33,    33,    34,    34,    35,    35,
    36,    37,    38,    39,    40,    41,    42,    43,
    44,    45,    46,    47,    48,    48,    49,    49,
    50,    51,    52,    53,    54,    55,    56,    57,
    58,    59,    60,    61,    62,    63,    64,    64,
    65,    66,    67,    68,    69,    70,    71,    72,
    73,    74,    75,    76,    77,    78,    79,    79,
    80,    81,    82,    83,    84,    85,    86,    87,
    88,    89,    90,    91,    92,    93,    94,    95,
    96,    97,    98,    99,    100,    101,    102,    103,
    104,    105,    106,    107,    108,    109,    110,    111,
    112,    113,    114,    115,    116,    117,    118,    119,
    120,    121,    122,    123,    124,    125,    126,    127};

/* see libst.h */
#ifdef    SUPERCEDED

static short ls_seg_end[8] = {0xFF, 0x1FF, 0x3FF, 0x7FF,
                0xFFF, 0x1FFF, 0x3FFF, 0x7FFF};

static short
ls_search(short val, short *table, short size)
{
    short        i;

    for (i = 0; i < size; i++) {
        if (val <= *table++)
            return (i);
    }
    return (size);
}

/*
 * linear2alaw() - Convert a 16-bit linear PCM value to 8-bit A-law
 *
 * linear2alaw() accepts an 16-bit integer and encodes it as A-law data.
 *
 *        Linear Input Code    Compressed Code
 *    ------------------------    ---------------
 *    0000000wxyza            000wxyz
 *    0000001wxyza            001wxyz
 *    000001wxyzab            010wxyz
 *    00001wxyzabc            011wxyz
 *    0001wxyzabcd            100wxyz
 *    001wxyzabcde            101wxyz
 *    01wxyzabcdef            110wxyz
 *    1wxyzabcdefg            111wxyz
 *
 * For further information see John C. Bellamy's Digital Telephony, 1982,
 * John Wiley & Sons, pps 98-111 and 472-476.
 */
unsigned char
ls_af_linear2alaw(short pcm_val)
    /* 2's complement (16-bit range) */
{
    short        mask;
    short        seg;
    unsigned char    aval;

    if (pcm_val >= 0) {
        mask = 0xD5;        /* sign (7th) bit = 1 */
    } else {
        mask = 0x55;        /* sign bit = 0 */
        pcm_val = -pcm_val-1;/* - 8;*/
        if(pcm_val<0)pcm_val = 32767;
    }

    /* Convert the scaled magnitude to segment number. */
    seg = ls_search(pcm_val, ls_seg_end, 8);

    /* Combine the sign, segment, and quantization bits. */

    if (seg >= 8)        /* out of range, return maximum value. */
        return (0x7F ^ mask);
    else {
        aval = seg << LS_SEG_SHIFT;
        if (seg < 2)
            aval |= (pcm_val >> 4) & LS_QUANT_MASK;
        else
            aval |= (pcm_val >> (seg + 3)) & LS_QUANT_MASK;
        return (aval ^ mask);
    }
}

/*
 * alaw2linear() - Convert an A-law value to 16-bit linear PCM
 *
 */
short
ls_af_alaw2linear(    unsigned char a_val)
    
{
    short        t;
    short        seg;

    a_val ^= 0x55;

    t = (a_val & LS_QUANT_MASK) << 4;
    seg = ((unsigned)a_val & LS_SEG_MASK) >> LS_SEG_SHIFT;
    switch (seg) {
    case 0:
        t += 8;
        break;
    case 1:
        t += 0x108;
        break;
    default:
        t += 0x108;
        t <<= seg - 1;
    }
    return ((a_val & LS_SIGN_BIT) ? t : -t);
}

#define    BIAS        (0x84)        /* Bias for linear code. */

/*
 * linear2ulaw() - Convert a linear PCM value to u-law
 *
 * In order to simplify the encoding process, the original linear magnitude
 * is biased by adding 33 which shifts the encoding range from (0 - 8158) to
 * (33 - 8191). The result can be seen in the following encoding table:
 *
 *    Biased Linear Input Code    Compressed Code
 *    ------------------------    ---------------
 *    00000001wxyza            000wxyz
 *    0000001wxyzab            001wxyz
 *    000001wxyzabc            010wxyz
 *    00001wxyzabcd            011wxyz
 *    0001wxyzabcde            100wxyz
 *    001wxyzabcdef            101wxyz
 *    01wxyzabcdefg            110wxyz
 *    1wxyzabcdefgh            111wxyz
 *
 * Each biased linear code has a leading 1 which identifies the segment
 * number. The value of the segment number is equal to 7 minus the number
 * of leading 0's. The quantization interval is directly available as the
 * four bits wxyz.  * The trailing bits (a - h) are ignored.
 *
 * Ordinarily the complement of the resulting code word is used for
 * transmission, and so the code word is complemented before it is returned.
 *
 * For further information see John C. Bellamy's Digital Telephony, 1982,
 * John Wiley & Sons, pps 98-111 and 472-476.
 */

/* 2's complement (16-bit range) */
unsigned char ls_af_linear2ulaw (short pcm_val)
{
    short        mask;
    short        seg;
    unsigned char    uval;

    /* Get the sign and the magnitude of the value. */
    if (pcm_val < 0) {
        pcm_val = BIAS - pcm_val;
        mask = 0x7F;
    } else {
        pcm_val += BIAS;
        mask = 0xFF;
    }

    /* Convert the scaled magnitude to segment number. */
    seg = ls_search(pcm_val, ls_seg_end, 8);

    /*
     * Combine the sign, segment, quantization bits;
     * and complement the code word.
     */
    if (seg >= 8)        /* out of range, return maximum value. */
        return (0x7F ^ mask);
    else {
        uval = (seg << 4) | ((pcm_val >> (seg + 3)) & 0xF);
        return (uval ^ mask);
    }

}

/*
 * ulaw2linear() - Convert a u-law value to 16-bit linear PCM
 *
 * First, a biased linear code is derived from the code word. An unbiased
 * output can then be obtained by subtracting 33 from the biased code.
 *
 * Note that this function expects to be passed the complement of the
 * original code word. This is in keeping with ISDN conventions.
 */
short ls_af_ulaw2linear (unsigned char u_val)
{
    short        t;

    /* Complement to obtain normal u-law value. */
    u_val = ~u_val;

    /*
     * Extract and bias the quantization bits. Then
     * shift up by the segment number and subtract out the bias.
     */
    t = ((u_val & LS_QUANT_MASK) << 3) + BIAS;
    t <<= ((unsigned)u_val & LS_SEG_MASK) >> LS_SEG_SHIFT;

    return ((u_val & LS_SIGN_BIT) ? (BIAS - t) : (t - BIAS));
}

#endif

/* A-law to u-law conversion */
unsigned char
ls_alaw2ulaw(unsigned char aval)
{
    aval &= 0xff;
    return ((aval & 0x80) ? (0xFF ^ ls_a2u[aval ^ 0xD5]) :
        (0x7F ^ ls_a2u[aval ^ 0x55]));
}

/* u-law to A-law conversion */
unsigned char
ls_ulaw2alaw(unsigned char uval)
{
    uval &= 0xff;
    return ((uval & 0x80) ? (0xD5 ^ (ls_u2a[0xFF ^ uval] - 1)) :
        (0x55 ^ (ls_u2a[0x7F ^ uval] - 1)));
}


//lawflag :0-A,1-U
void LSG711Encoder(short *pcm,unsigned char *code,int size,int lawflag)
{
    int i;

    if(lawflag==0){
        for(i=0;i<size;i++){
            code[i]=ls_af_linear2alaw(pcm[i]);
        }
    }
    else{
        for(i=0;i<size;i++){
            code[i]=ls_af_linear2ulaw(pcm[i]);
        }
    }
}


void LSG711Decoder(short *pcm,unsigned char *code,int size,int lawflag)
{
    int i;

    if(lawflag==0){
        for(i=0;i<size;i++){
            pcm[i]=ls_af_alaw2linear(code[i]);
        }
    }
    else{
        for(i=0;i<size;i++){
            pcm[i]=ls_af_ulaw2linear(code[i]);
        }
    }
}



//
//  FWMP3ToAACConvertor.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/8.
//

import AVFoundation


class FWMP3ToAACConvertor: NSObject {
    
    private var reader: AVAssetReader?
    private var readerOutput: AVAssetReaderTrackOutput?
    private var writer: AVAssetWriter?
    private var writerInput: AVAssetWriterInput?
    private let processingQueue = DispatchQueue(label: "processingQueue")
    
    convenience init(inputURL: URL, outputURL: URL) {
        self.init()
        let asset = AVAsset(url: inputURL)
        reader = try? AVAssetReader(asset: asset)
        if let audioTrack = asset.tracks(withMediaType: .audio).first {
            readerOutput = AVAssetReaderTrackOutput(track: audioTrack, outputSettings: [AVFormatIDKey: Int(kAudioFormatLinearPCM)])
        }
        writer = try? AVAssetWriter(outputURL: outputURL, fileType: .m4a)
        writerInput = AVAssetWriterInput(mediaType: AVMediaType.audio,
                                                              outputSettings: [
                                                                AVFormatIDKey: kAudioFormatMPEG4AAC,
                                                                AVSampleRateKey: 8000,
                                                                AVNumberOfChannelsKey: 1])
    }
    
    func startConvert(_ completion: @escaping (Bool) -> Void) {
        guard let reader, let readerOutput, let writer, let writerInput else { return }
        reader.add(readerOutput)
        reader.startReading()
        writer.add(writerInput)
        writer.startWriting()
        writer.startSession(atSourceTime: .zero)
        
        writerInput.requestMediaDataWhenReady(on: processingQueue) {
            while writerInput.isReadyForMoreMediaData {
                guard let nextBuffer = readerOutput.copyNextSampleBuffer() else {
                    writerInput.markAsFinished()
                    writer.finishWriting(completionHandler: {
                        if writer.status == .completed {
                            completion(true)
                            print("File converted successfully")
                        } else {
                            completion(false)
                            print("File conversion failed with error: \(writer.error?.localizedDescription ?? "unknown error")")
                        }
                    })
                    break
                }
                writerInput.append(nextBuffer)
            }
        }
    }
}



#import <UIKit/UIKit.h>

@interface PixelRenderView : UIView

@property (nonatomic, assign) NSInteger rotation;
@property (nonatomic, assign, nullable) CGImageRef image;

- (void)displayPixelBuffer:(CVPixelBufferRef _Nullable)pixelBuffer;


- (nullable UIImage *)snapImage;

@end



#import "PixelRenderView.h"
#import <QuartzCore/QuartzCore.h>
#import <AVFoundation/AVUtilities.h>
#import <mach/mach_time.h>
#import <GLKit/GLKit.h>

#define SHADER_STRING(x) #x
static const char *vertexShader = SHADER_STRING
(
attribute vec4 position;
attribute vec2 texCoord;

varying vec2 texCoordVarying;

void main()
{
 float preferredRotation = 3.14;
 mat4 rotationMatrix = mat4( cos(preferredRotation), -sin(preferredRotation), 0.0, 0.0,
                            sin(preferredRotation),  cos(preferredRotation), 0.0, 0.0,
                            0.0,                        0.0, 1.0, 0.0,
                            0.0,                        0.0, 0.0, 1.0);
 gl_Position = rotationMatrix * position;
 texCoordVarying = texCoord;
}
);
static const char *fragmentShader = SHADER_STRING
(
varying highp vec2 texCoordVarying;
precision mediump float;

uniform sampler2D SamplerY;
uniform sampler2D SamplerUV;
uniform mat3 colorConversionMatrix;

void main()
{
 mediump vec3 yuv;
 lowp vec3 rgb;
 
 yuv.x = (texture2D(SamplerY, texCoordVarying).r);
 yuv.yz = (texture2D(SamplerUV, texCoordVarying).ra - vec2(0.5, 0.5));
 
 rgb = colorConversionMatrix * yuv;

 gl_FragColor = vec4(rgb,1);
}
);

// Uniform index.
enum
{
    UNIFORM_Y,
    UNIFORM_UV,
    UNIFORM_COLOR_CONVERSION_MATRIX,
    NUM_UNIFORMS
};
static GLint uniforms[NUM_UNIFORMS];

// Attribute index.
enum
{
    ATTRIB_VERTEX,
    ATTRIB_TEXCOORD,
    NUM_ATTRIBUTES
};

// Color Conversion Constants (YUV to RGB) including adjustment from 16-235/16-240 (video range)

// BT.601, which is the standard for SDTV.
static const GLfloat kColorConversion601[] = {
        1.164,  1.164, 1.164,
          0.0, -0.392, 2.017,
        1.596, -0.813,   0.0,
};

// BT.709, which is the standard for HDTV.
static const GLfloat kColorConversion709[] = {
        1.164,  1.164, 1.164,
          0.0, -0.213, 2.112,
        1.793, -0.533,   0.0,
};

// BT.601 full range (ref: http://www.equasys.de/colorconversion.html)
const GLfloat kColorConversion601FullRange[] = {
    1.0,    1.0,    1.0,
    0.0,    -0.343, 1.765,
    1.4,    -0.711, 0.0,
};


@interface PixelRenderView ()
{
    // The pixel dimensions of the CAEAGLLayer.
    GLint _backingWidth;
    GLint _backingHeight;

    EAGLContext *_context;
    CVOpenGLESTextureRef _lumaTexture;
    CVOpenGLESTextureRef _chromaTexture;
    CVOpenGLESTextureCacheRef _videoTextureCache;
    
    GLuint _frameBufferHandle;
    GLuint _colorBufferHandle;
    
    const GLfloat *_preferredConversion;
}

@property GLuint program;
@property CGRect lastBounds;

- (void)setupBuffers;
- (void)cleanUpTextures;

- (BOOL)loadShaders;
- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type sourceString:(const GLchar *)source;
- (BOOL)linkProgram:(GLuint)prog;
- (BOOL)validateProgram:(GLuint)prog;

@end

@implementation PixelRenderView

+ (Class)layerClass
{
    return [CAEAGLLayer class];
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    if ((self = [super initWithCoder:aDecoder]))
    {
        self.contentScaleFactor = [[UIScreen mainScreen] scale];

        CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;

        eaglLayer.opaque = TRUE;
        eaglLayer.drawableProperties = @{ kEAGLDrawablePropertyRetainedBacking :[NSNumber numberWithBool:NO],
                                          kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8};

        
        _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
        
        if (!_context || ![EAGLContext setCurrentContext:_context] || ![self loadShaders]) {
            return nil;
        }
        _preferredConversion = kColorConversion709;
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.lastBounds = self.bounds;
        self.contentScaleFactor = [[UIScreen mainScreen] scale];
        CAEAGLLayer *eaglLayer = (CAEAGLLayer *)self.layer;

        eaglLayer.opaque = TRUE;
        eaglLayer.drawableProperties = @{ kEAGLDrawablePropertyRetainedBacking :[NSNumber numberWithBool:NO],
                                          kEAGLDrawablePropertyColorFormat : kEAGLColorFormatRGBA8};

        _context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];

        if (!_context || ![EAGLContext setCurrentContext:_context] || ![self loadShaders]) {
            return nil;
        }
        _preferredConversion = kColorConversion709;
    }
    return self;
}

- (void)didMoveToSuperview {
    [super didMoveToSuperview];
    if (self.superview) {
        [self setupGL];
    }
}

- (void)layoutSubviews {
    if (!CGRectEqualToRect(self.bounds, self.lastBounds)) {
        [self resizeFromLayer];
        self.lastBounds = self.bounds;
    }
}

- (void)setImage:(CGImageRef)image {
    _image = image;
    self.layer.contents = (__bridge id _Nullable)(image);
}

- (nullable UIImage *)snapImage {
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, YES, 0.0);
    [self drawViewHierarchyInRect:self.bounds afterScreenUpdates:YES];
    UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}

# pragma mark - OpenGL setup

- (void)setupGL
{
    [EAGLContext setCurrentContext:_context];
    [self setupBuffers];
    [self loadShaders];
    
    glUseProgram(self.program);
    
    glUniform1i(uniforms[UNIFORM_Y], 0);
    glUniform1i(uniforms[UNIFORM_UV], 1);
    
    glUniformMatrix3fv(uniforms[UNIFORM_COLOR_CONVERSION_MATRIX], 1, GL_FALSE, _preferredConversion);
    
    // Create CVOpenGLESTextureCacheRef for optimal CVPixelBufferRef to GLES texture conversion.
    if (!_videoTextureCache) {
        CVReturn err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_videoTextureCache);
        if (err != noErr) {
            NSLog(@"Error at CVOpenGLESTextureCacheCreate %d", err);
            return;
        }
    }
}

#pragma mark - Utilities

- (void)setupBuffers
{
    glDisable(GL_DEPTH_TEST);
    
    glEnableVertexAttribArray(ATTRIB_VERTEX);
    glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
    
    glEnableVertexAttribArray(ATTRIB_TEXCOORD);
    glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), 0);
    
    glGenFramebuffers(1, &_frameBufferHandle);
    glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferHandle);
    
    glGenRenderbuffers(1, &_colorBufferHandle);
    glBindRenderbuffer(GL_RENDERBUFFER, _colorBufferHandle);
    
    [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_backingWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_backingHeight);

    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorBufferHandle);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        NSLog(@"Failed to make complete framebuffer object1 %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
    }
}

- (void)resizeFromLayer {
    glBindRenderbuffer(GL_RENDERBUFFER, _colorBufferHandle);
    [_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:(CAEAGLLayer *)self.layer];
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_backingWidth);
    glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_backingHeight);

    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorBufferHandle);
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
        NSLog(@"Failed to make complete framebuffer object2 %x", glCheckFramebufferStatus(GL_FRAMEBUFFER));
    }
}

- (void)cleanUpTextures
{
    if (_lumaTexture) {
        CFRelease(_lumaTexture);
        _lumaTexture = NULL;
    }
    
    if (_chromaTexture) {
        CFRelease(_chromaTexture);
        _chromaTexture = NULL;
    }
    
    // Periodic texture cache flush every frame
    CVOpenGLESTextureCacheFlush(_videoTextureCache, 0);
}

- (void)dealloc
{
    APMethodLog
    [self cleanUpTextures];
    
    if(_videoTextureCache) {
        CFRelease(_videoTextureCache);
    }
}

#pragma mark - OpenGLES drawing

- (void)displayPixelBuffer:(CVPixelBufferRef)pixelBuffer
{
    CVReturn err;
    if (pixelBuffer != NULL) {
        int frameWidth = (int)CVPixelBufferGetWidth(pixelBuffer);
        int frameHeight = (int)CVPixelBufferGetHeight(pixelBuffer);
        
        if (!_videoTextureCache) {
            NSLog(@"No video texture cache");
            return;
        }
        [EAGLContext setCurrentContext:_context];
        [self cleanUpTextures];
        
        /*
         Use the color attachment of the pixel buffer to determine the appropriate color conversion matrix.
         */
        CFTypeRef colorAttachments = CVBufferGetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, NULL);
        _preferredConversion = kColorConversion601FullRange;
        
        /*
         CVOpenGLESTextureCacheCreateTextureFromImage will create GLES texture optimally from CVPixelBufferRef.
         */
        
        /*
         Create Y and UV textures from the pixel buffer. These textures will be drawn on the frame buffer Y-plane.
         */
        glActiveTexture(GL_TEXTURE0);
        err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                           _videoTextureCache,
                                                           pixelBuffer,
                                                           NULL,
                                                           GL_TEXTURE_2D,
                                                           GL_LUMINANCE,
                                                           frameWidth,
                                                           frameHeight,
                                                           GL_LUMINANCE,
                                                           GL_UNSIGNED_BYTE,
                                                           0,
                                                           &_lumaTexture);
        if (err) {
            NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
        }
        
        glBindTexture(CVOpenGLESTextureGetTarget(_lumaTexture), CVOpenGLESTextureGetName(_lumaTexture));
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        
        // UV-plane.
        glActiveTexture(GL_TEXTURE1);
        err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
                                                           _videoTextureCache,
                                                           pixelBuffer,
                                                           NULL,
                                                           GL_TEXTURE_2D,
                                                           GL_LUMINANCE_ALPHA,
                                                           frameWidth / 2,
                                                           frameHeight / 2,
                                                           GL_LUMINANCE_ALPHA,
                                                           GL_UNSIGNED_BYTE,
                                                           1,
                                                           &_chromaTexture);
        if (err) {
            NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
        }
        
        glBindTexture(CVOpenGLESTextureGetTarget(_chromaTexture), CVOpenGLESTextureGetName(_chromaTexture));
//        NSLog(@"id %d", CVOpenGLESTextureGetName(_chromaTexture));
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        
        glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferHandle);
        
        // Set the view port to the entire view.
        glViewport(0, 0, _backingWidth, _backingHeight);
    }
    
    glClearColor(0.1f, 0.0f, 0.0f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    
    // Use shader program.
    glUseProgram(self.program);
    glUniformMatrix3fv(uniforms[UNIFORM_COLOR_CONVERSION_MATRIX], 1, GL_FALSE, _preferredConversion);
    
    // Set up the quad vertices with respect to the orientation and aspect ratio of the video.
    CGRect vertexSamplingRect = AVMakeRectWithAspectRatioInsideRect(CGSizeMake(_backingWidth, _backingHeight), self.layer.bounds);
    
    // Compute normalized quad coordinates to draw the frame into.
    CGSize normalizedSamplingSize = CGSizeMake(0.0, 0.0);
    CGSize cropScaleAmount = CGSizeMake(vertexSamplingRect.size.width/self.layer.bounds.size.width, vertexSamplingRect.size.height/self.layer.bounds.size.height);
    
    // Normalize the quad vertices.
    if (cropScaleAmount.width > cropScaleAmount.height) {
        normalizedSamplingSize.width = 1.0;
        normalizedSamplingSize.height = cropScaleAmount.height/cropScaleAmount.width;
    }
    else {
        normalizedSamplingSize.width = 1.0;
        normalizedSamplingSize.height = cropScaleAmount.width/cropScaleAmount.height;
    }
    
    /*
     The quad vertex data defines the region of 2D plane onto which we draw our pixel buffers.
     Vertex data formed using (-1,-1) and (1,1) as the bottom left and top right coordinates respectively, covers the entire screen.
     */
    GLfloat lt, rt, lb, rb, lt1, rt1, lb1, rb1;
    if (_rotation == 90) {
        lt = -1; rt = -1; lb = 1; rb = 1;
        lt1 = 1; rt1 = -1; lb1 = 1; rb1 = -1;
    }else if (_rotation == 180) {
        lt = 1; rt = -1; lb = 1; rb = -1;
        lt1 = 1; rt1 = 1; lb1 = -1; rb1 = -1;
    }else if (_rotation == 270) {
        lt = 1; rt = 1; lb = -1; rb = -1;
        lt1 = -1; rt1 = 1; lb1 = -1; rb1 = 1;
    }else {
        lt = -1; rt = 1; lb = -1; rb = 1;
        lt1 = -1; rt1 = -1; lb1 = 1; rb1 = 1;
    }
    GLfloat quadVertexData [] = {
        lt * normalizedSamplingSize.width, lt1 * normalizedSamplingSize.height,
        rt * normalizedSamplingSize.width, rt1 * normalizedSamplingSize.height,
        lb * normalizedSamplingSize.width, lb1 * normalizedSamplingSize.height,
        rb * normalizedSamplingSize.width, rb1 * normalizedSamplingSize.height,
    };
    
    // 更新顶点数据
    glVertexAttribPointer(ATTRIB_VERTEX, 2, GL_FLOAT, 0, 0, quadVertexData);
    glEnableVertexAttribArray(ATTRIB_VERTEX);
    
    GLfloat quadTextureData[] =  { // 正常坐标
        1, 0,
        0, 0,
        1, 1,
        0, 1
    };
    
    glVertexAttribPointer(ATTRIB_TEXCOORD, 2, GL_FLOAT, 0, 0, quadTextureData);
    glEnableVertexAttribArray(ATTRIB_TEXCOORD);
    
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    glBindRenderbuffer(GL_RENDERBUFFER, _colorBufferHandle);
    
    [_context presentRenderbuffer:GL_RENDERBUFFER];
    
}

#pragma mark -  OpenGL ES 2 shader compilation

- (BOOL)loadShaders
{
    GLuint vertShader, fragShader;
    self.program = glCreateProgram();
    // Create and compile the vertex shader.
    if (![self compileShader:&vertShader type:GL_VERTEX_SHADER sourceString:(const GLchar *)vertexShader]) {
        NSLog(@"Failed to compile vertex shader");
        return NO;
    }
    
    // Create and compile fragment shader.
    if (![self compileShader:&fragShader type:GL_FRAGMENT_SHADER sourceString:(const GLchar *)fragmentShader]) {
        NSLog(@"Failed to compile fragment shader");
        return NO;
    }
    
    // Attach vertex shader to program.
    glAttachShader(self.program, vertShader);
    
    // Attach fragment shader to program.
    glAttachShader(self.program, fragShader);
    
    // Bind attribute locations. This needs to be done prior to linking.
    glBindAttribLocation(self.program, ATTRIB_VERTEX, "position");
    glBindAttribLocation(self.program, ATTRIB_TEXCOORD, "texCoord");
    
    // Link the program.
    if (![self linkProgram:self.program]) {
        NSLog(@"Failed to link program: %d", self.program);
        
        if (vertShader) {
            glDeleteShader(vertShader);
            vertShader = 0;
        }
        if (fragShader) {
            glDeleteShader(fragShader);
            fragShader = 0;
        }
        if (self.program) {
            glDeleteProgram(self.program);
            self.program = 0;
        }
        
        return NO;
    }
    
    // Get uniform locations.
    uniforms[UNIFORM_Y] = glGetUniformLocation(self.program, "SamplerY");
    uniforms[UNIFORM_UV] = glGetUniformLocation(self.program, "SamplerUV");
    uniforms[UNIFORM_COLOR_CONVERSION_MATRIX] = glGetUniformLocation(self.program, "colorConversionMatrix");
    
    // Release vertex and fragment shaders.
    if (vertShader) {
        glDetachShader(self.program, vertShader);
        glDeleteShader(vertShader);
    }
    if (fragShader) {
        glDetachShader(self.program, fragShader);
        glDeleteShader(fragShader);
    }
    
    return YES;
}

- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type sourceString:(const GLchar *)source
{

    GLint status;
    *shader = glCreateShader(type);
    glShaderSource(*shader, 1, &source, NULL);
    glCompileShader(*shader);
    
    glGetShaderiv(*shader, GL_COMPILE_STATUS, &status);
    if (status == 0) {
        glDeleteShader(*shader);
        return NO;
    }
    
    return YES;
}

- (BOOL)linkProgram:(GLuint)prog
{
    GLint status;
    glLinkProgram(prog);
    
    glGetProgramiv(prog, GL_LINK_STATUS, &status);
    if (status == 0) {
        return NO;
    }
    
    return YES;
}

- (BOOL)validateProgram:(GLuint)prog
{
    GLint logLength, status;
    
    glValidateProgram(prog);
    glGetProgramiv(prog, GL_INFO_LOG_LENGTH, &logLength);
    if (logLength > 0) {
        GLchar *log = (GLchar *)malloc(logLength);
        glGetProgramInfoLog(prog, logLength, &logLength, log);
        NSLog(@"Program validate log:\n%s", log);
        free(log);
    }
    
    glGetProgramiv(prog, GL_VALIDATE_STATUS, &status);
    if (status == 0) {
        return NO;
    }
    
    return YES;
}

@end


//
//  SampleBufferDisplayView.h
//  LancensLockDemo
//
//  Created by 付庆平 on 2022/2/25.
//

#import <UIKit/UIKit.h>
#import <AVFoundation/AVSampleBufferDisplayLayer.h>

NS_ASSUME_NONNULL_BEGIN

@interface SampleBufferDisplayView : UIImageView

@property (nonatomic, strong, readonly) AVSampleBufferDisplayLayer *displayLayer;

- (void)render:(nullable CMSampleBufferRef)sampleBuffer;

- (void)clear;


@end

NS_ASSUME_NONNULL_END

//
//  SampleBufferDisplayView.m
//  LancensLockDemo
//
//  Created by 付庆平 on 2022/2/25.
//

#import "SampleBufferDisplayView.h"

@interface SampleBufferDisplayView ()

@end

@implementation SampleBufferDisplayView

+ (Class)layerClass {
    return AVSampleBufferDisplayLayer.class;
}

- (AVSampleBufferDisplayLayer *)displayLayer {
    return (AVSampleBufferDisplayLayer *)self.layer;
}

- (void)render:(nullable CMSampleBufferRef)sampleBuffer {
    if (sampleBuffer == NULL) return;
    if ([self.displayLayer isReadyForMoreMediaData]) {
        if (self.displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
            [self clear];
        }
        [self.displayLayer enqueueSampleBuffer:sampleBuffer];
    }
}

- (void)clear {
    [self.displayLayer flushAndRemoveImage];
    self.image = nil;
}

- (void)dealloc {
    [self clear];
}


@end

//
//  APVTVideoDecoder.h
//  arpha plus
//
//  Created by michael on 2023/3/21.
//

#import <UIKit/UIKit.h>
#import <CoreMedia/CMSampleBuffer.h>


NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, APNalUnitType) {
    APNalUnitType_VPS,
    APNalUnitType_SPS,
    APNalUnitType_PPS,
    APNalUnitType_IDR,
    APNalUnitType_PFM,
    APNalUnitType_OTR
};

typedef NS_ENUM(NSUInteger, APVideoEncodeType) {
    APVideoEncodeType_H264,
    APVideoEncodeType_H265
};

@class APP2PPacket;
@interface APH26XNalUnit : NSObject

@property (nonatomic, assign, readonly) APVideoEncodeType encodeType;
@property (nonatomic, assign, readonly) APNalUnitType type;
@property (nonatomic,   copy, readonly) NSData *rawData;
@property (nonatomic,   copy, readonly) NSData *avccData;
///主要是为了携带信息
@property (nonatomic, strong, nullable) APP2PPacket *pkt;


- (instancetype)initWithBuffer:(uint8_t *)buffer bufferSize:(int)bufferSize packet:(APP2PPacket *)pkt;

+ (nullable NSArray<APH26XNalUnit *> *)parseUnitWithPacket:(APP2PPacket *)pkt;

@end

@protocol APVTVideoDecoderDelegate <NSObject>

@optional

- (void)decoderOutputSampleBuffer:(nullable CMSampleBufferRef)sampleBuffer pixelBuffer:(nullable CVPixelBufferRef)pixelBuffer isDecoded:(BOOL)isDecoded packet:(APP2PPacket *)pkt;

- (void)decoderOutputSnapShot:(UIImage *)image;

@end


typedef void(^DecodeBufferBlock)(CMSampleBufferRef _Nullable sampleBuffer, BOOL isDecoded);
typedef void(^SnapshotImageBlock)(UIImage *_Nullable);

@interface APVTVideoDecoder : NSObject

@property (nonatomic, weak) id<APVTVideoDecoderDelegate> delegate;

@property (nonatomic, assign, readonly) CMVideoFormatDescriptionRef videoFormat;

/// 不去解码，使用`AVSampleBufferDisplayLayer`作为渲染前端 ，默认为NO
@property (nonatomic, assign) BOOL avoidDecode;

- (instancetype)initWithEncodeType:(APVideoEncodeType)encodeType delegate:(id<APVTVideoDecoderDelegate>)delegate;

@property (nonatomic, assign) BOOL reinit;

- (void)decodeWithPacket:(APP2PPacket *)pkt;

- (void)snapshot;

- (UIImage *)imageWithPixelBuffer:(CVPixelBufferRef)pixelBuffer;

@end

NS_ASSUME_NONNULL_END

//
//  APVTVideoDecoder.m
//  arpha plus
//
//  Created by michael on 2023/3/21.
//

#import "APVTVideoDecoder.h"
#import <CoreMedia/CoreMedia.h>
#import <VideoToolbox/VideoToolbox.h>
#import "APP2PPacket.h"
#import <AVFoundation/AVFCore.h>


@interface APH26XNalUnit ()

@property (nonatomic, assign) APVideoEncodeType encodeType;
@property (nonatomic, assign) APNalUnitType type;
@property (nonatomic,   copy) NSData *rawData;
@property (nonatomic,   copy) NSData *avccData;


@end

@implementation APH26XNalUnit


- (instancetype)initWithBuffer:(uint8_t *)buffer bufferSize:(int)bufferSize packet:(APP2PPacket *)pkt {
    if (self = [super init]) {
        APVideoEncodeType encodeType = pkt.encodeType == 2;
        self.encodeType = encodeType;
        self.rawData = [NSData dataWithBytes:buffer length:bufferSize];
        int nalSize = CFSwapInt32HostToBig(bufferSize - 4);
        uint8_t *avccBuffer = (uint8_t *)malloc(bufferSize);
        memcpy(avccBuffer, buffer, bufferSize);
        memcpy(avccBuffer, &nalSize, sizeof(nalSize));
        self.avccData = [NSData dataWithBytes:avccBuffer length:bufferSize];
        free(avccBuffer);
        APNalUnitType type = APNalUnitType_OTR;
        int typeValue;
        if (encodeType == APVideoEncodeType_H264) {
            typeValue = (buffer + 4)[0] & 0x1f;
            switch (typeValue) {
                case 0x01:
                case 0x02:
                case 0x03:
                case 0x04:
                    type = APNalUnitType_PFM;
                    break;
                case 0x05:
                    type = APNalUnitType_IDR;
                    break;
                case 0x07:
                    type = APNalUnitType_SPS;
                    break;
                case 0x08:
                    type = APNalUnitType_PPS;
                    break;
                default:
                    break;
            }
        }else {
            typeValue = ((buffer + 4)[0] & 0x7E) >> 1;
            switch (typeValue) {
                case 0x01:
                    type = APNalUnitType_PFM;
                    break;
                case 0x13:
                    type = APNalUnitType_IDR;
                    break;
                case 0x20:
                    type = APNalUnitType_VPS;
                    break;
                case 0x21:
                    type = APNalUnitType_SPS;
                    break;
                case 0x22:
                    type = APNalUnitType_PPS;
                    break;
                default:
                    break;
            }
        }
//        NSString *typeStr = nil;
//        if (type == APNalUnitType_VPS) {
//            typeStr = @"vps";
//        }else if (type == APNalUnitType_SPS) {
//            typeStr = @"sps";
//        }else if (type == APNalUnitType_PPS) {
//            typeStr = @"pps";
//        }
//        if (typeStr) {
//            NSLog(@"nalu %@ data: %@",typeStr, DataHexString([NSData dataWithBytes:buffer + 4 length:bufferSize - 4]));
//        }
        /// 只对I帧和P帧 pkt才赋值，避免内存占用
        if (type == APNalUnitType_IDR || type == APNalUnitType_PFM) {
            self.pkt = pkt;
        }else {
            self.pkt = nil;
        }
        self.type = type;
    }
    return self;
}

+ (NSArray<APH26XNalUnit *> *)parseUnitWithPacket:(APP2PPacket *)pkt {
    uint8_t *buffer = (uint8_t *)pkt.pktData.bytes;
    int bufferSize = (int)pkt.pktData.length;
    if (bufferSize <= 4) return nil;
    NSMutableArray *nalUnits = [NSMutableArray array];
    NSMutableArray *indexes = [NSMutableArray array];
    for (int i = 0; i < bufferSize - 4; i++) {
        if (isStart(buffer+i)) {
            [indexes addObject:@(i)];
        }
    }
    for (int i = 0; i < indexes.count; i++) {
        int index = [indexes[i] intValue];
        uint8_t *buf = buffer + index;
        int bufSize = 0;
        if (i == indexes.count - 1) {
            bufSize = bufferSize - index;
        }else {
            bufSize = [indexes[i + 1] intValue] - index;
        }
        APH26XNalUnit *nalUnit = [[APH26XNalUnit alloc] initWithBuffer:buf bufferSize:bufSize packet:pkt];
        [nalUnits addObject:nalUnit];
    }
    return [nalUnits copy];
}

NS_INLINE BOOL isStart(uint8_t *buffer) {
    return buffer[0]==0&&buffer[1]==0&&buffer[2]==0&&buffer[3]==1;
}

- (BOOL)isEqual:(APH26XNalUnit *)object {
    if (![object isKindOfClass:[APH26XNalUnit class]]) return NO;
    return [self.rawData isEqualToData:object.rawData];
}

@end

@interface APVTVideoDecoder()

@property (nonatomic, strong) APH26XNalUnit *spsNalUnit;
@property (nonatomic, strong) APH26XNalUnit *ppsNalUnit;
@property (nonatomic, strong) APH26XNalUnit *vpsNalUnit;

@property (nonatomic, assign) APVideoEncodeType encodeType;

@property (nonatomic, assign) CMVideoFormatDescriptionRef videoFormat;
@property (nonatomic, assign) VTDecompressionSessionRef decompressionSession;
@property (nonatomic, assign) BOOL snapshotFlag;
@property (nonatomic, assign) UInt64 startTimestamp;


@end

@implementation APVTVideoDecoder


- (instancetype)initWithEncodeType:(APVideoEncodeType)encodeType delegate:(nonnull id<APVTVideoDecoderDelegate>)delegate {
    if (self = [super init]) {
        _encodeType = encodeType;
        _avoidDecode = NO;
        _delegate = delegate;
    }
    return self;
}


///解码回调
static void decompressCallback(void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef pixelBuffer, CMTime presentationTimeStamp, CMTime presentationDuration) {
    CVPixelBufferRef *outputPixelBuffer = (CVPixelBufferRef *)sourceFrameRefCon;
    *outputPixelBuffer = CVPixelBufferRetain(pixelBuffer);
}



- (void)decodeWithPacket:(APP2PPacket *)pkt {
    NSArray <APH26XNalUnit *> *nalUnits = [APH26XNalUnit parseUnitWithPacket:pkt];
    APVideoEncodeType encodeType = pkt.encodeType == 2;
    if (encodeType != _encodeType) {
        _encodeType = encodeType;
        _reinit = YES;
    }
    for (APH26XNalUnit *nalUnit in nalUnits) {
        switch (nalUnit.type) {
            case APNalUnitType_VPS:
            {
                if (_vpsNalUnit && ![_vpsNalUnit isEqual:nalUnit]) {
                    _reinit = YES;
                }else {
                    _vpsNalUnit = nalUnit;
                }
            }
                break;
            case APNalUnitType_SPS:
            {
                if (_spsNalUnit && ![_spsNalUnit isEqual:nalUnit]) {
                    _reinit = YES;
                }else {
                    _spsNalUnit = nalUnit;
                }
            }
                break;
            case APNalUnitType_PPS:
            {
                if (_ppsNalUnit && ![_ppsNalUnit isEqual:nalUnit]) {
                    _reinit = YES;
                }else {
                    _ppsNalUnit = nalUnit;
                }
            }
                break;
            case APNalUnitType_IDR:
            {
                if ([self initVideoFormat]) {
                    [self decodeWithNalUnit:nalUnit];
                }
            }
                break;
            case APNalUnitType_PFM:
            {
                [self decodeWithNalUnit:nalUnit];
            }
                break;
            default:
                break;
        }
    }
}

- (void)decodeWithNalUnit:(APH26XNalUnit *)unit {
    if (_videoFormat == NULL) return;
    uint8_t *buffer = (uint8_t *)unit.avccData.bytes;
    int length = (int)unit.avccData.length;
    CMBlockBufferRef blockBuffer = NULL;
    CMSampleBufferRef sampleBuffer = NULL;
    OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, (void *)buffer, length, kCFAllocatorNull, NULL, 0, length, 0, &blockBuffer);
    if (status == noErr && blockBuffer) {
        const size_t sampleSize[] = { length };
        if (self.startTimestamp == 0) {
            self.startTimestamp = unit.pkt.pts;
        }
        CMTime pts = CMTimeMake(66667, AP_TIME_BASE);
        CMSampleTimingInfo timingInfo = {.duration = kCMTimeZero, .presentationTimeStamp = pts, .decodeTimeStamp = kCMTimeInvalid};
        status = CMSampleBufferCreateReady(kCFAllocatorDefault, blockBuffer, _videoFormat, 1, 1, &timingInfo, 1, sampleSize, &sampleBuffer);
        if (status == noErr && sampleBuffer && self.avoidDecode) {
            CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
            CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
            CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
        }
    }
    if (self.avoidDecode) {
        
        if ([self.delegate respondsToSelector:@selector(decoderOutputSampleBuffer:pixelBuffer:isDecoded:packet:)]) {
            [self.delegate decoderOutputSampleBuffer:sampleBuffer pixelBuffer:NULL isDecoded:NO packet:unit.pkt];
        }
    }else {
        CVPixelBufferRef pixelBuffer = NULL;
        if ([self initDecompressSession]) {
            pixelBuffer = [self decompressH264SampleBuffer:sampleBuffer];
            if (self.snapshotFlag) {
                self.snapshotFlag = NO;
                if ([self.delegate respondsToSelector:@selector(decoderOutputSnapShot:)]) {
                    [self.delegate decoderOutputSnapShot:[self imageWithPixelBuffer:pixelBuffer]];
                }
            }
            if ([self.delegate respondsToSelector:@selector(decoderOutputSampleBuffer:pixelBuffer:isDecoded:packet:)]) {
                [self.delegate decoderOutputSampleBuffer:sampleBuffer pixelBuffer:pixelBuffer isDecoded:YES packet:unit.pkt];
            }
        }
        CFSafeRelease(pixelBuffer);
    }
    CFSafeRelease(sampleBuffer);
    CFSafeRelease(blockBuffer);
}


- (void)snapshot {
    self.snapshotFlag = YES;
}

- (UIImage *)imageWithPixelBuffer:(CVPixelBufferRef)pixelBuffer {
    CIImage *ciimage = [CIImage imageWithCVPixelBuffer:pixelBuffer];
    CIContext *cictx = [CIContext contextWithOptions:nil];
    CGImageRef cgimage = [cictx createCGImage:ciimage fromRect:CGRectMake(0, 0, CVPixelBufferGetWidth(pixelBuffer), CVPixelBufferGetHeight(pixelBuffer))];
    UIImage *image = [[UIImage alloc] initWithCGImage:cgimage scale:1.0 orientation:UIImageOrientationUp];
    CGImageRelease(cgimage);
    return image;
}

NS_INLINE void CFSafeRelease(CFTypeRef cf) {
    if (cf) CFRelease(cf);
}

- (BOOL)initVideoFormat {
    if (_reinit) {
        _reinit = NO;
        [self releaseVideoFormat];
        [self releaseDecompressSession];
    }
    if (self.encodeType == APVideoEncodeType_H264) {
        if (!_spsNalUnit || !_ppsNalUnit) return NO;
    }else {
        if (!_spsNalUnit || !_ppsNalUnit || !_vpsNalUnit) return NO;
    }
    if (_videoFormat) return YES;
    OSStatus status = noErr;
    if (self.encodeType == APVideoEncodeType_H264) {
        const uint8_t *const psPointers[2] = {(_spsNalUnit.rawData.bytes + 4), (_ppsNalUnit.rawData.bytes + 4)};
        const size_t psLengths[2] = {(_spsNalUnit.rawData.length - 4), (_ppsNalUnit.rawData.length - 4)};
        status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, psPointers, psLengths, 4, &_videoFormat);
    }else {
        const uint8_t *const psPointers[3] = {(_vpsNalUnit.rawData.bytes + 4), (_spsNalUnit.rawData.bytes + 4), (_ppsNalUnit.rawData.bytes + 4)};
        const size_t psLengths[3] = {(_vpsNalUnit.rawData.length - 4), (_spsNalUnit.rawData.length - 4), (_ppsNalUnit.rawData.length - 4)};
        status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault, 3, psPointers, psLengths, 4, NULL, &_videoFormat);
    }
    if (status != noErr) {
        DDLogError(@"vtbTag: video format init error: %d",(int)status);
    }
    return status == noErr;
}

///初始化解码器
- (BOOL)initDecompressSession {
    if (_decompressionSession && _videoFormat) return YES;
    if (_videoFormat == NULL) return NO;
    uint32_t pixelFormatType = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange;  //420p
    const void *keys[] = { kCVPixelBufferPixelFormatTypeKey };
    const void *values[] = { CFNumberCreate(NULL, kCFNumberSInt32Type, &pixelFormatType) };
    CFDictionaryRef attrs = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
    
    VTDecompressionOutputCallbackRecord callBackRecord;
    callBackRecord.decompressionOutputCallback = decompressCallback;
    callBackRecord.decompressionOutputRefCon = NULL;
    
    OSStatus status = VTDecompressionSessionCreate(kCFAllocatorDefault,
                                          _videoFormat,
                                          NULL, attrs,
                                          &callBackRecord,
                                          &_decompressionSession);
    if (status != noErr) {
        DDLogError(@"vtbTag: video decode session init error: %d",(int)status);
    }
    CFRelease(attrs);
    return status == noErr;
}

- (CVPixelBufferRef)decompressH264SampleBuffer:(CMSampleBufferRef)sampleBuffer {
    VTDecodeFrameFlags flags = 0;
    VTDecodeInfoFlags flagOut = 0;
    CVPixelBufferRef outputPixelBuffer = NULL;
    OSStatus decodeStatus = VTDecompressionSessionDecodeFrame(_decompressionSession, sampleBuffer, flags, &outputPixelBuffer, &flagOut);
    if (decodeStatus == kVTVideoDecoderMalfunctionErr) {
        DDLogError(@"vtbTag: Decode failed status: kVTVideoDecoderMalfunctionErr");
        CVPixelBufferRelease(outputPixelBuffer);
        outputPixelBuffer = NULL;
    } else if(decodeStatus == kVTInvalidSessionErr) {
        DDLogError(@"vtbTag: Invalid session, reset decoder session");
        [self releaseDecompressSession];
    } else if(decodeStatus == kVTVideoDecoderBadDataErr) {
        DDLogError(@"vtbTag: Decode failed status=%d(Bad data)", (int)decodeStatus);
    } else if(decodeStatus != noErr) {
        DDLogError(@"vtbTag: Decode failed status=%d", (int)decodeStatus);
    }else {
//        NSLog(@"Decode success");
    }
    
    return outputPixelBuffer;
}

- (void)releaseVideoFormat {
    if (_videoFormat) {
        _vpsNalUnit = nil;
        _spsNalUnit = nil;
        _ppsNalUnit = nil;
        CFRelease(_videoFormat);
        _videoFormat = NULL;
        _startTimestamp = 0;
    }
}

- (void)clear {
    [self releaseVideoFormat];
    [self releaseDecompressSession];
}

- (void)releaseDecompressSession {
    if (_decompressionSession) {
        VTDecompressionSessionInvalidate(_decompressionSession);
        CFRelease(_decompressionSession);
        _decompressionSession = NULL;
    }
}

- (void)dealloc {
    [self clear];
}




@end

//
//  VideoRecorder.h
//  arpha plus
//
//  Created by michael on 2023/8/31.
//

#import <Foundation/Foundation.h>

#import <CoreMedia/CMFormatDescription.h>
#import <CoreMedia/CMSampleBuffer.h>

@protocol VideoRecorderDelegate;



@interface VideoRecorder : NSObject

- (instancetype)initWithURL:(NSURL *)URL;

// Only one audio and video track each are allowed.
- (void)addVideoTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription transform:(CGAffineTransform)transform settings:(NSDictionary *)videoSettings; // see AVVideoSettings.h for settings keys/values
- (void)addAudioTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription settings:(NSDictionary *)audioSettings; // see AVAudioSettings.h for settings keys/values

- (void)setDelegate:(id<VideoRecorderDelegate>)delegate callbackQueue:(dispatch_queue_t)delegateCallbackQueue; // delegate is weak referenced

- (void)prepareToRecord; // Asynchronous, might take several hundred milliseconds. When finished the delegate's recorderDidFinishPreparing: or recorder:didFailWithError: method will be called.

- (void)appendVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)appendVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime;
- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer;

- (void)finishRecording; // Asynchronous, might take several hundred milliseconds. When finished the delegate's recorderDidFinishRecording: or recorder:didFailWithError: method will be called.

@end

@protocol VideoRecorderDelegate <NSObject>
@required
- (void)videoRecorderDidFinishPreparing:(VideoRecorder *)recorder;
- (void)videoRecorder:(VideoRecorder *)recorder didFailWithError:(NSError *)error;
- (void)videoRecorderDidFinishRecording:(VideoRecorder *)recorder;
@end

//
//  VideoRecorder.m
//  arpha plus
//
//  Created by michael on 2023/8/31.
//

#import "VideoRecorder.h"

#import <AVFoundation/AVAssetWriter.h>
#import <AVFoundation/AVAssetWriterInput.h>

#import <AVFoundation/AVMediaFormat.h>
#import <AVFoundation/AVVideoSettings.h>
#import <AVFoundation/AVAudioSettings.h>

typedef NS_ENUM( NSInteger, VideoRecorderStatus ) {
    VideoRecorderStatusIdle = 0,
    VideoRecorderStatusPreparingToRecord,
    VideoRecorderStatusRecording,
    VideoRecorderStatusFinishingRecordingPart1, // waiting for inflight buffers to be appended
    VideoRecorderStatusFinishingRecordingPart2, // calling finish writing on the asset writer
    VideoRecorderStatusFinished,    // terminal state
    VideoRecorderStatusFailed        // terminal state
}; // internal state machine


@interface VideoRecorder ()
{
    VideoRecorderStatus _status;

    dispatch_queue_t _delegateCallbackQueue;
    
    dispatch_queue_t _writingQueue;
    
    NSURL *_URL;
    
    AVAssetWriter *_assetWriter;
    BOOL _haveStartedSession;
    
    CMFormatDescriptionRef _audioTrackSourceFormatDescription;
    NSDictionary *_audioTrackSettings;
    AVAssetWriterInput *_audioInput;
    
    CMFormatDescriptionRef _videoTrackSourceFormatDescription;
    CGAffineTransform _videoTrackTransform;
    NSDictionary *_videoTrackSettings;
    AVAssetWriterInput *_videoInput;
}
@property (nonatomic,weak) id <VideoRecorderDelegate> delegate;
@end

@implementation VideoRecorder

#pragma mark -
#pragma mark API

- (instancetype)initWithURL:(NSURL *)URL
{
    if ( ! URL ) {
        return nil;
    }
    
    self = [super init];
    if ( self ) {
        _writingQueue = dispatch_queue_create( "com.apple.sample.VideoRecorder.writing", DISPATCH_QUEUE_SERIAL );
        _videoTrackTransform = CGAffineTransformIdentity;
        _URL = URL;
    }
    return self;
}

- (void)addVideoTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription transform:(CGAffineTransform)transform settings:(NSDictionary *)videoSettings
{
    if ( formatDescription == NULL ) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"NULL format description" userInfo:nil];
        return;
    }
    
    @synchronized( self )
    {
        if ( _status != VideoRecorderStatusIdle ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add tracks while not idle" userInfo:nil];
            return;
        }
        
        if ( _videoTrackSourceFormatDescription ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add more than one video track" userInfo:nil];
            return;
        }
        
        _videoTrackSourceFormatDescription = (CMFormatDescriptionRef)CFRetain( formatDescription );
        _videoTrackTransform = transform;
        _videoTrackSettings = [videoSettings copy];
    }
}

- (void)addAudioTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription settings:(NSDictionary *)audioSettings
{
    @synchronized( self )
    {
        if ( _status != VideoRecorderStatusIdle ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add tracks while not idle" userInfo:nil];
            return;
        }
        
        if ( _audioTrackSourceFormatDescription ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add more than one audio track" userInfo:nil];
            return;
        }
        
        _audioTrackSourceFormatDescription = (CMFormatDescriptionRef)CFRetain( formatDescription );
        _audioTrackSettings = [audioSettings copy];
    }
    
}

- (id<VideoRecorderDelegate>)delegate
{
    id <VideoRecorderDelegate> delegate = nil;
    @synchronized( self ) {
        delegate = _delegate;
    }
    return delegate;
}

- (void)setDelegate:(id<VideoRecorderDelegate>)delegate callbackQueue:(dispatch_queue_t)delegateCallbackQueue; // delegate is weak referenced
{
    if ( delegate && ( delegateCallbackQueue == NULL ) ) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Caller must provide a delegateCallbackQueue" userInfo:nil];
    }
    
    @synchronized( self )
    {
        _delegate = delegate;
        if ( delegateCallbackQueue != _delegateCallbackQueue  ) {
            _delegateCallbackQueue = delegateCallbackQueue;
        }
    }
}

- (void)prepareToRecord
{
    @synchronized( self )
    {
        if ( _status != VideoRecorderStatusIdle ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Already prepared, cannot prepare again" userInfo:nil];
            return;
        }
        
        [self transitionToStatus:VideoRecorderStatusPreparingToRecord error:nil];
    }
    
    dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0 ), ^{
        
        @autoreleasepool
        {
            NSError *error = nil;
            // AVAssetWriter will not write over an existing file.
            [[NSFileManager defaultManager] removeItemAtURL:self->_URL error:NULL];
            
            self->_assetWriter = [[AVAssetWriter alloc] initWithURL:self->_URL fileType:AVFileTypeQuickTimeMovie error:&error];
            
            // Create and add inputs
            if ( ! error && self->_videoTrackSourceFormatDescription ) {
                [self setupAssetWriterVideoInputWithSourceFormatDescription:self->_videoTrackSourceFormatDescription transform:self->_videoTrackTransform settings:self->_videoTrackSettings error:&error];
            }
            
            if ( ! error && self->_audioTrackSourceFormatDescription ) {
                [self setupAssetWriterAudioInputWithSourceFormatDescription:self->_audioTrackSourceFormatDescription settings:self->_audioTrackSettings error:&error];
            }
            
            if ( ! error ) {
                BOOL success = [self->_assetWriter startWriting];
                if ( ! success ) {
                    error = self->_assetWriter.error;
                }
            }
            
            @synchronized( self )
            {
                if ( error ) {
                    [self transitionToStatus:VideoRecorderStatusFailed error:error];
                }
                else {
                    [self transitionToStatus:VideoRecorderStatusRecording error:nil];
                }
            }
        }
    } );
}

- (void)appendVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    [self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeVideo];
}

- (void)appendVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime
{
    CMSampleBufferRef sampleBuffer = NULL;
    
    CMSampleTimingInfo timingInfo = {0,};
    timingInfo.duration = kCMTimeInvalid;
    timingInfo.decodeTimeStamp = kCMTimeInvalid;
    timingInfo.presentationTimeStamp = presentationTime;
    if ( sampleBuffer ) {
        [self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeVideo];
        CFRelease( sampleBuffer );
    }
    else {
//        NSString *exceptionReason = [NSString stringWithFormat:@"sample buffer create failed (%i)", (int)err];
//        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:exceptionReason userInfo:nil];
//        return;
    }
}

- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    [self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeAudio];
}

- (void)finishRecording
{
    @synchronized( self )
    {
        BOOL shouldFinishRecording = NO;
        switch ( _status )
        {
            case VideoRecorderStatusIdle:
            case VideoRecorderStatusPreparingToRecord:
            case VideoRecorderStatusFinishingRecordingPart1:
            case VideoRecorderStatusFinishingRecordingPart2:
            case VideoRecorderStatusFinished:
                @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Not recording" userInfo:nil];
                break;
            case VideoRecorderStatusFailed:
                // From the client's perspective the movie recorder can asynchronously transition to an error state as the result of an append.
                // Because of this we are lenient when finishRecording is called and we are in an error state.
                NSLog( @"Recording has failed, nothing to do" );
                break;
            case VideoRecorderStatusRecording:
                shouldFinishRecording = YES;
                break;
        }
        
        if ( shouldFinishRecording ) {
            [self transitionToStatus:VideoRecorderStatusFinishingRecordingPart1 error:nil];
        }
        else {
            return;
        }
    }
    
    dispatch_async( _writingQueue, ^{
        
        @autoreleasepool
        {
            @synchronized( self )
            {
                // We may have transitioned to an error state as we appended inflight buffers. In that case there is nothing to do now.
                if ( self->_status != VideoRecorderStatusFinishingRecordingPart1 ) {
                    return;
                }
                
                // It is not safe to call -[AVAssetWriter finishWriting*] concurrently with -[AVAssetWriterInput appendSampleBuffer:]
                // We transition to VideoRecorderStatusFinishingRecordingPart2 while on _writingQueue, which guarantees that no more buffers will be appended.
                [self transitionToStatus:VideoRecorderStatusFinishingRecordingPart2 error:nil];
            }

            [self->_assetWriter finishWritingWithCompletionHandler:^{
                @synchronized( self )
                {
                    NSError *error = self->_assetWriter.error;
                    if ( error ) {
                        [self transitionToStatus:VideoRecorderStatusFailed error:error];
                    }
                    else {
                        [self transitionToStatus:VideoRecorderStatusFinished error:nil];
                    }
                }
            }];
        }
    } );
}

- (void)dealloc
{
    _delegate = nil;
    [self teardownAssetWriterAndInputs];
    if ( _audioTrackSourceFormatDescription ) {
        CFRelease( _audioTrackSourceFormatDescription );
    }
    if ( _videoTrackSourceFormatDescription ) {
        CFRelease( _videoTrackSourceFormatDescription );
    }
    APMethodLog
}

#pragma mark -
#pragma mark Internal

- (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer ofMediaType:(NSString *)mediaType
{
    if ( sampleBuffer == NULL ) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"NULL sample buffer" userInfo:nil];
        return;
    }
    
    @synchronized( self ) {
        if ( _status < VideoRecorderStatusRecording ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Not ready to record yet" userInfo:nil];
            return;
        }
    }
    
    CFRetain( sampleBuffer );
    dispatch_async( _writingQueue, ^{
        
        @autoreleasepool
        {
            @synchronized( self )
            {
                // From the client's perspective the movie recorder can asynchronously transition to an error state as the result of an append.
                // Because of this we are lenient when samples are appended and we are no longer recording.
                // Instead of throwing an exception we just release the sample buffers and return.
                if ( self->_status > VideoRecorderStatusFinishingRecordingPart1 ) {
                    CFRelease( sampleBuffer );
                    return;
                }
            }
            
            if ( ! self->_haveStartedSession ) {
                CMTime sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
                [self->_assetWriter startSessionAtSourceTime:sourceTime];
                self->_haveStartedSession = YES;
            }
            
            AVAssetWriterInput *input = ( mediaType == AVMediaTypeVideo ) ? self->_videoInput : self->_audioInput;
            if ( input.readyForMoreMediaData )
            {
                BOOL success = [input appendSampleBuffer:sampleBuffer];
                if ( ! success ) {
                    NSError *error = self->_assetWriter.error;
                    @synchronized( self ) {
                        [self transitionToStatus:VideoRecorderStatusFailed error:error];
                    }
                }
            }
            else
            {
                NSLog( @"%@ input not ready for more media data, dropping buffer", mediaType );
            }
            CFRelease( sampleBuffer );
        }
    } );
}

// call under @synchonized( self )
- (void)transitionToStatus:(VideoRecorderStatus)newStatus error:(NSError *)error
{
    BOOL shouldNotifyDelegate = NO;
    
    if ( newStatus != _status )
    {
        // terminal states
        if ( ( newStatus == VideoRecorderStatusFinished ) || ( newStatus == VideoRecorderStatusFailed ) )
        {
            shouldNotifyDelegate = YES;
            // make sure there are no more sample buffers in flight before we tear down the asset writer and inputs
            
            dispatch_async( _writingQueue, ^{
                [self teardownAssetWriterAndInputs];
                if ( newStatus == VideoRecorderStatusFailed ) {
                    [[NSFileManager defaultManager] removeItemAtURL:self->_URL error:NULL];
                }
            } );
        }
        else if ( newStatus == VideoRecorderStatusRecording )
        {
            shouldNotifyDelegate = YES;
        }
        
        _status = newStatus;
    }

    if ( shouldNotifyDelegate && self.delegate )
    {
        dispatch_async( _delegateCallbackQueue, ^{
            
            @autoreleasepool
            {
                switch ( newStatus )
                {
                    case VideoRecorderStatusRecording:
                        [self.delegate videoRecorderDidFinishPreparing:self];
                        break;
                    case VideoRecorderStatusFinished:
                        [self.delegate videoRecorderDidFinishRecording:self];
                        break;
                    case VideoRecorderStatusFailed:
                        [self.delegate videoRecorder:self didFailWithError:error];
                        break;
                    default:
                        break;
                }
            }
        } );
    }
}


- (BOOL)setupAssetWriterAudioInputWithSourceFormatDescription:(CMFormatDescriptionRef)audioFormatDescription settings:(NSDictionary *)audioSettings error:(NSError **)errorOut
{
    
    if ( ! audioSettings ) {
        NSLog( @"No audio settings provided, using default settings" );
//        audioSettings = @{ AVFormatIDKey : @(kAudioFormatMPEG4AAC),
//                           AVSampleRateKey: @8000,
//                           AVNumberOfChannelsKey: @1
//        };
        audioSettings = @{ AVFormatIDKey : @(kAudioFormatULaw),
                           AVSampleRateKey: @8000,
                           AVNumberOfChannelsKey: @1
        };
    }
    
    if ( [_assetWriter canApplyOutputSettings:audioSettings forMediaType:AVMediaTypeAudio] )
    {
//        _audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings sourceFormatHint:audioFormatDescription];
        _audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:nil sourceFormatHint:audioFormatDescription];
        _audioInput.expectsMediaDataInRealTime = YES;
        
        if ( [_assetWriter canAddInput:_audioInput] )
        {
            [_assetWriter addInput:_audioInput];
        }
        else
        {
            if ( errorOut ) {
                *errorOut = [[self class] cannotSetupInputError];
            }
            return NO;
        }
    }
    else
    {
        NSLog(@"recordTag: add audio error");
        if ( errorOut ) {
            *errorOut = [[self class] cannotSetupInputError];
        }
        return NO;
    }
    
    return YES;
}

- (BOOL)setupAssetWriterVideoInputWithSourceFormatDescription:(CMFormatDescriptionRef)videoFormatDescription transform:(CGAffineTransform)transform settings:(NSDictionary *)videoSettings error:(NSError **)errorOut
{
    if ( ! videoSettings )
    {
        float bitsPerPixel;
        CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions( videoFormatDescription );
        int numPixels = dimensions.width * dimensions.height;
        int bitsPerSecond;
    
        NSLog( @"No video settings provided, using default settings" );
        
        // Assume that lower-than-SD resolutions are intended for streaming, and use a lower bitrate
        if ( numPixels < ( 640 * 480 ) ) {
            bitsPerPixel = 1.0; // This bitrate approximately matches the quality produced by AVCaptureSessionPresetMedium or Low.
        }
        else {
            bitsPerPixel = 1.5; // This bitrate approximately matches the quality produced by AVCaptureSessionPresetHigh.
        }
        
        bitsPerSecond = numPixels * bitsPerPixel;
        
        NSDictionary *compressionProperties = @{ AVVideoAverageBitRateKey : @(bitsPerSecond),
                                                 AVVideoExpectedSourceFrameRateKey : @(15),
                                                 AVVideoMaxKeyFrameIntervalKey : @(15) };
        
        videoSettings = @{ AVVideoCodecKey : AVVideoCodecTypeH264,
                           AVVideoWidthKey : @(dimensions.width),
                           AVVideoHeightKey : @(dimensions.height),
                           AVVideoCompressionPropertiesKey : compressionProperties };
    }
    
    if ( [_assetWriter canApplyOutputSettings:videoSettings forMediaType:AVMediaTypeVideo] )
    {
//        _videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSettings sourceFormatHint:videoFormatDescription];
        _videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:nil sourceFormatHint:videoFormatDescription];
        _videoInput.expectsMediaDataInRealTime = YES;
        _videoInput.transform = transform;
        
        if ( [_assetWriter canAddInput:_videoInput] )
        {
            [_assetWriter addInput:_videoInput];
        }
        else
        {
            if ( errorOut ) {
                *errorOut = [[self class] cannotSetupInputError];
            }
            return NO;
        }
    }
    else
    {
        if ( errorOut ) {
            *errorOut = [[self class] cannotSetupInputError];
        }
        return NO;
    }
    
    return YES;
}

+ (NSError *)cannotSetupInputError
{
    NSString *localizedDescription = NSLocalizedString( @"Recording cannot be started", nil );
    NSString *localizedFailureReason = NSLocalizedString( @"Cannot setup asset writer input.", nil );
    NSDictionary *errorDict = @{ NSLocalizedDescriptionKey : localizedDescription,
                                 NSLocalizedFailureReasonErrorKey : localizedFailureReason };
    return [NSError errorWithDomain:@"com.apple.dts.samplecode" code:0 userInfo:errorDict];
}

- (void)teardownAssetWriterAndInputs
{
    _videoInput = nil;
    _audioInput = nil;
    _assetWriter = nil;
}

@end


/*
     File: MovieRecorder.m
 Abstract: Real-time movie recorder which is totally non-blocking
  Version: 2.1
 
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
 Inc. ("Apple") in consideration of your agreement to the following
 terms, and your use, installation, modification or redistribution of
 this Apple software constitutes acceptance of these terms.  If you do
 not agree with these terms, please do not use, install, modify or
 redistribute this Apple software.
 
 In consideration of your agreement to abide by the following terms, and
 subject to these terms, Apple grants you a personal, non-exclusive
 license, under Apple's copyrights in this original Apple software (the
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
 Software, with or without modifications, in source and/or binary forms;
 provided that if you redistribute the Apple Software in its entirety and
 without modifications, you must retain this notice and the following
 text and disclaimers in all such redistributions of the Apple Software.
 Neither the name, trademarks, service marks or logos of Apple Inc. may
 be used to endorse or promote products derived from the Apple Software
 without specific prior written permission from Apple.  Except as
 expressly stated in this notice, no other rights or licenses, express or
 implied, are granted by Apple herein, including but not limited to any
 patent rights that may be infringed by your derivative works or by other
 works in which the Apple Software may be incorporated.
 
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
 Copyright (C) 2014 Apple Inc. All Rights Reserved.
 
 */

#import "MovieRecorder.h"
#import <AVFoundation/AVAssetWriter.h>
#import <AVFoundation/AVAssetWriterInput.h>
#import <AVFoundation/AVMediaFormat.h>
#import <AVFoundation/AVVideoSettings.h>
#import <AVFoundation/AVAudioSettings.h>
#include <objc/runtime.h> // for objc_loadWeak() and objc_storeWeak()
#define LOG_STATUS_TRANSITIONS 0
typedef NS_ENUM( NSInteger, MovieRecorderStatus ) {
    MovieRecorderStatusIdle = 0,
    MovieRecorderStatusPreparingToRecord,
    MovieRecorderStatusRecording,
    MovieRecorderStatusFinishingRecordingPart1, // waiting for inflight buffers to be appended
    MovieRecorderStatusFinishingRecordingPart2, // calling finish writing on the asset writer
    MovieRecorderStatusFinished,    // terminal state
    MovieRecorderStatusFailed        // terminal state
}; // internal state machine
@interface MovieRecorder ()
{
    MovieRecorderStatus _status;
    id <MovieRecorderDelegate> _delegate; // __weak doesn't actually do anything under non-ARC
    dispatch_queue_t _delegateCallbackQueue;
    
    dispatch_queue_t _writingQueue;
    
    NSURL *_URL;
    
    AVAssetWriter *_assetWriter;
    BOOL _haveStartedSession;
    
    CMFormatDescriptionRef _audioTrackSourceFormatDescription;
    NSDictionary *_audioTrackSettings;
    AVAssetWriterInput *_audioInput;
    
    CMFormatDescriptionRef _videoTrackSourceFormatDescription;
    CGAffineTransform _videoTrackTransform;
    NSDictionary *_videoTrackSettings;
    AVAssetWriterInput *_videoInput;
}
//@property (nonatomic,weak) id <MovieRecorderDelegate> delegate;
@end
@implementation MovieRecorder
#pragma mark -
#pragma mark API
- (instancetype)initWithURL:(NSURL *)URL
{
    if ( ! URL ) {
        [self release];
        return nil;
    }
    
    self = [super init];
    if ( self ) {
        _writingQueue = dispatch_queue_create( "com.apple.sample.movierecorder.writing", DISPATCH_QUEUE_SERIAL );
        _videoTrackTransform = CGAffineTransformIdentity;
        _URL = [URL retain];
    }
    return self;
}
- (void)addVideoTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription transform:(CGAffineTransform)transform settings:(NSDictionary *)videoSettings
{
    if ( formatDescription == NULL ) {
//        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"NULL format description" userInfo:nil];
//        return;
    }
    
    @synchronized( self )
    {
        if ( _status != MovieRecorderStatusIdle ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add tracks while not idle" userInfo:nil];
            return;
        }
        
        if ( _videoTrackSourceFormatDescription ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add more than one video track" userInfo:nil];
            return;
        }
        
        _videoTrackSourceFormatDescription = (CMFormatDescriptionRef)CFRetain( formatDescription );
        _videoTrackTransform = transform;
        _videoTrackSettings = [videoSettings copy];
    }
}
- (void)addAudioTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription settings:(NSDictionary *)audioSettings
{
    if ( formatDescription == NULL ) {
//        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"NULL format description" userInfo:nil];
//        return;
    }
    
    else {
        @synchronized( self )
        {
            if ( _status != MovieRecorderStatusIdle ) {
                @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add tracks while not idle" userInfo:nil];
                return;
            }
            
            if ( _audioTrackSourceFormatDescription ) {
                @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Cannot add more than one audio track" userInfo:nil];
                return;
            }
            
            _audioTrackSourceFormatDescription = (CMFormatDescriptionRef)CFRetain( formatDescription );
            _audioTrackSettings = [audioSettings copy];
        }
    }
    
}
- (id<MovieRecorderDelegate>)delegate
{
    id <MovieRecorderDelegate> delegate = nil;
    @synchronized( self ) {
        delegate = objc_loadWeak( &_delegate ); // unnecessary under ARC, just assign to delegate directly
    }
    return delegate;
}
- (void)setDelegate:(id<MovieRecorderDelegate>)delegate callbackQueue:(dispatch_queue_t)delegateCallbackQueue; // delegate is weak referenced
{
    if ( delegate && ( delegateCallbackQueue == NULL ) ) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Caller must provide a delegateCallbackQueue" userInfo:nil];
    }
    
    @synchronized( self )
    {
        objc_storeWeak( &_delegate, delegate ); // unnecessary under ARC, just assign to _delegate directly
        if ( delegateCallbackQueue != _delegateCallbackQueue  ) {
            [_delegateCallbackQueue release];
            _delegateCallbackQueue = [delegateCallbackQueue retain];
        }
    }
}
- (void)prepareToRecord
{
    @synchronized( self )
    {
        if ( _status != MovieRecorderStatusIdle ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Already prepared, cannot prepare again" userInfo:nil];
            return;
        }
        
        [self transitionToStatus:MovieRecorderStatusPreparingToRecord error:nil];
    }
    
    dispatch_async( dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_LOW, 0 ), ^{
        
        @autoreleasepool
        {
            NSError *error = nil;
            // AVAssetWriter will not write over an existing file.
            [[NSFileManager defaultManager] removeItemAtURL:_URL error:NULL];
            
            _assetWriter = [[AVAssetWriter alloc] initWithURL:_URL fileType:AVFileTypeMPEG4 error:&error];
            
            // Create and add inputs
            if ( ! error && _videoTrackSourceFormatDescription ) {
                [self setupAssetWriterVideoInputWithSourceFormatDescription:_videoTrackSourceFormatDescription transform:_videoTrackTransform settings:_videoTrackSettings error:&error];
            }
            
            if ( ! error && _audioTrackSourceFormatDescription ) {
                [self setupAssetWriterAudioInputWithSourceFormatDescription:_audioTrackSourceFormatDescription settings:_audioTrackSettings error:&error];
            }
            
            if ( ! error ) {
                BOOL success = [_assetWriter startWriting];
                if ( ! success ) {
                    error = _assetWriter.error;
                }
            }
            
            @synchronized( self )
            {
                if ( error ) {
                    [self transitionToStatus:MovieRecorderStatusFailed error:error];
                }
                else {
                    [self transitionToStatus:MovieRecorderStatusRecording error:nil];
                }
            }
        }
    } );
}
- (void)appendVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    [self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeVideo];
}
- (void)appendVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime
{
    CMSampleBufferRef sampleBuffer = NULL;
    
    CMSampleTimingInfo timingInfo = {0,};
    timingInfo.duration = kCMTimeInvalid;
    timingInfo.decodeTimeStamp = kCMTimeInvalid;
    timingInfo.presentationTimeStamp = presentationTime;
    
    
    OSStatus err = CMSampleBufferCreateForImageBuffer( kCFAllocatorDefault, pixelBuffer, true, NULL, NULL, _videoTrackSourceFormatDescription, &timingInfo, &sampleBuffer );
    if ( sampleBuffer ) {
        [self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeVideo];
        CFRelease( sampleBuffer );
    }
    else {
//        NSString *exceptionReason = [NSString stringWithFormat:@"sample buffer create failed (%i)", (int)err];
//        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:exceptionReason userInfo:nil];
//        return;
    }
}
- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer
{
    [self appendSampleBuffer:sampleBuffer ofMediaType:AVMediaTypeAudio];
}
- (void)finishRecording
{
    @synchronized( self )
    {
        BOOL shouldFinishRecording = NO;
        switch ( _status )
        {
            case MovieRecorderStatusIdle:
            case MovieRecorderStatusPreparingToRecord:
            case MovieRecorderStatusFinishingRecordingPart1:
            case MovieRecorderStatusFinishingRecordingPart2:
            case MovieRecorderStatusFinished:
                @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Not recording" userInfo:nil];
                break;
            case MovieRecorderStatusFailed:
                // From the client's perspective the movie recorder can asynchronously transition to an error state as the result of an append.
                // Because of this we are lenient when finishRecording is called and we are in an error state.
                NSLog( @"Recording has failed, nothing to do" );
                break;
            case MovieRecorderStatusRecording:
                shouldFinishRecording = YES;
                break;
        }
        
        if ( shouldFinishRecording ) {
            [self transitionToStatus:MovieRecorderStatusFinishingRecordingPart1 error:nil];
        }
        else {
            return;
        }
    }
    
    dispatch_async( _writingQueue, ^{
        
        @autoreleasepool
        {
            @synchronized( self )
            {
                // We may have transitioned to an error state as we appended inflight buffers. In that case there is nothing to do now.
                if ( _status != MovieRecorderStatusFinishingRecordingPart1 ) {
                    return;
                }
                
                // It is not safe to call -[AVAssetWriter finishWriting*] concurrently with -[AVAssetWriterInput appendSampleBuffer:]
                // We transition to MovieRecorderStatusFinishingRecordingPart2 while on _writingQueue, which guarantees that no more buffers will be appended.
                [self transitionToStatus:MovieRecorderStatusFinishingRecordingPart2 error:nil];
            }
            [_assetWriter finishWritingWithCompletionHandler:^{
                @synchronized( self )
                {
                    NSError *error = _assetWriter.error;
                    if ( error ) {
                        [self transitionToStatus:MovieRecorderStatusFailed error:error];
                    }
                    else {
                        [self transitionToStatus:MovieRecorderStatusFinished error:nil];
                    }
                }
            }];
        }
    } );
}
- (void)dealloc
{
    objc_storeWeak( &_delegate, nil ); // unregister _delegate as a weak reference
    
    [_delegateCallbackQueue release];
    
    [_writingQueue release];
    
    [self teardownAssetWriterAndInputs];
    if ( _audioTrackSourceFormatDescription ) {
        CFRelease( _audioTrackSourceFormatDescription );
    }
    [_audioTrackSettings release];
    
    if ( _videoTrackSourceFormatDescription ) {
        CFRelease( _videoTrackSourceFormatDescription );
    }
    [_videoTrackSettings release];
    [_URL release];
    APMethodLog
    [super dealloc];
    
}
#pragma mark -
#pragma mark Internal
- (void)appendSampleBuffer:(CMSampleBufferRef)sampleBuffer ofMediaType:(NSString *)mediaType
{
    if ( sampleBuffer == NULL ) {
        @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"NULL sample buffer" userInfo:nil];
        return;
    }
    
    @synchronized( self ) {
        if ( _status < MovieRecorderStatusRecording ) {
            @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Not ready to record yet" userInfo:nil];
            return;
        }
    }
    
    CFRetain( sampleBuffer );
    dispatch_async( _writingQueue, ^{
        
        @autoreleasepool
        {
            @synchronized( self )
            {
                // From the client's perspective the movie recorder can asynchronously transition to an error state as the result of an append.
                // Because of this we are lenient when samples are appended and we are no longer recording.
                // Instead of throwing an exception we just release the sample buffers and return.
                if ( _status > MovieRecorderStatusFinishingRecordingPart1 ) {
                    CFRelease( sampleBuffer );
                    return;
                }
            }
            
            if ( ! _haveStartedSession ) {
                CMTime sourceTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer);
                [_assetWriter startSessionAtSourceTime:sourceTime];
                _haveStartedSession = YES;
            }
            
            AVAssetWriterInput *input = ( mediaType == AVMediaTypeVideo ) ? _videoInput : _audioInput;
            
            if ( input.readyForMoreMediaData )
            {
                BOOL success = [input appendSampleBuffer:sampleBuffer];
                if ( ! success ) {
                    NSError *error = _assetWriter.error;
                    @synchronized( self ) {
                        [self transitionToStatus:MovieRecorderStatusFailed error:error];
                    }
                }
            }
            else
            {
                NSLog( @"%@ input not ready for more media data, dropping buffer", mediaType );
            }
            CFRelease( sampleBuffer );
        }
    } );
}
// call under @synchonized( self )
- (void)transitionToStatus:(MovieRecorderStatus)newStatus error:(NSError *)error
{
    BOOL shouldNotifyDelegate = NO;
    
#if LOG_STATUS_TRANSITIONS
    NSLog( @"MovieRecorder state transition: %@->%@", [self stringForStatus:_status], [self stringForStatus:newStatus] );
#endif
    if ( newStatus != _status )
    {
        // terminal states
        if ( ( newStatus == MovieRecorderStatusFinished ) || ( newStatus == MovieRecorderStatusFailed ) )
        {
            shouldNotifyDelegate = YES;
            // make sure there are no more sample buffers in flight before we tear down the asset writer and inputs
            
            dispatch_async( _writingQueue, ^{
                [self teardownAssetWriterAndInputs];
                if ( newStatus == MovieRecorderStatusFailed ) {
                    [[NSFileManager defaultManager] removeItemAtURL:_URL error:NULL];
                }
            } );
#if LOG_STATUS_TRANSITIONS
            if ( error ) {
                NSLog( @"MovieRecorder error: %@, code: %i", error, (int)error.code );
            }
#endif
        }
        else if ( newStatus == MovieRecorderStatusRecording )
        {
            shouldNotifyDelegate = YES;
        }
        _status = newStatus;
    }
    if ( shouldNotifyDelegate && self.delegate )
    {
        dispatch_async( _delegateCallbackQueue, ^{
            
            @autoreleasepool
            {
                switch ( newStatus )
                {
                    case MovieRecorderStatusRecording:
                        [self.delegate movieRecorderDidFinishPreparing:self];
                        break;
                    case MovieRecorderStatusFinished:
                        [self.delegate movieRecorderDidFinishRecording:self];
                        break;
                    case MovieRecorderStatusFailed:
                        [self.delegate movieRecorder:self didFailWithError:error];
                        break;
                    default:
                        break;
                }
            }
        } );
    }
}
#if LOG_STATUS_TRANSITIONS
- (NSString *)stringForStatus:(MovieRecorderStatus)status
{
    NSString *statusString = nil;
    
    switch ( status )
    {
        case MovieRecorderStatusIdle:
            statusString = @"Idle";
            break;
        case MovieRecorderStatusPreparingToRecord:
            statusString = @"PreparingToRecord";
            break;
        case MovieRecorderStatusRecording:
            statusString = @"Recording";
            break;
        case MovieRecorderStatusFinishingRecordingPart1:
            statusString = @"FinishingRecordingPart1";
            break;
        case MovieRecorderStatusFinishingRecordingPart2:
            statusString = @"FinishingRecordingPart2";
            break;
        case MovieRecorderStatusFinished:
            statusString = @"Finished";
            break;
        case MovieRecorderStatusFailed:
            statusString = @"Failed";
            break;
        default:
            statusString = @"Unknown";
            break;
    }
    return statusString;
    
}
#endif // LOG_STATUS_TRANSITIONS
- (BOOL)setupAssetWriterAudioInputWithSourceFormatDescription:(CMFormatDescriptionRef)audioFormatDescription settings:(NSDictionary *)audioSettings error:(NSError **)errorOut
{
    if ( ! audioSettings ) {
        NSLog( @"No audio settings provided, using default settings" );
        audioSettings = @{ AVFormatIDKey : @(kAudioFormatMPEG4AAC),
                           AVSampleRateKey: @8000,
                           AVNumberOfChannelsKey: @1
        };
    }
    
    if ( [_assetWriter canApplyOutputSettings:audioSettings forMediaType:AVMediaTypeAudio] )
    {
        _audioInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeAudio outputSettings:audioSettings sourceFormatHint:audioFormatDescription];
        _audioInput.expectsMediaDataInRealTime = YES;
        
        if ( [_assetWriter canAddInput:_audioInput] )
        {
            [_assetWriter addInput:_audioInput];
        }
        else
        {
            if ( errorOut ) {
                *errorOut = [[self class] cannotSetupInputError];
            }
            return NO;
        }
    }
    else
    {
        if ( errorOut ) {
            *errorOut = [[self class] cannotSetupInputError];
        }
        return NO;
    }
    
    return YES;
}
- (BOOL)setupAssetWriterVideoInputWithSourceFormatDescription:(CMFormatDescriptionRef)videoFormatDescription transform:(CGAffineTransform)transform settings:(NSDictionary *)videoSettings error:(NSError **)errorOut
{
    if ( ! videoSettings )
    {
        float bitsPerPixel;
        CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions( videoFormatDescription );
        int numPixels = dimensions.width * dimensions.height;
        int bitsPerSecond;
    
        NSLog( @"No video settings provided, using default settings" );
        
        // Assume that lower-than-SD resolutions are intended for streaming, and use a lower bitrate
        if ( numPixels < ( 640 * 480 ) ) {
            bitsPerPixel = 1.0; // This bitrate approximately matches the quality produced by AVCaptureSessionPresetMedium or Low.
        }
        else {
            bitsPerPixel = 1.5; // This bitrate approximately matches the quality produced by AVCaptureSessionPresetHigh.
        }
        
        bitsPerSecond = numPixels * bitsPerPixel;
        
        NSDictionary *compressionProperties = @{ AVVideoAverageBitRateKey : @(bitsPerSecond),
                                                 AVVideoExpectedSourceFrameRateKey : @(15),
                                                 AVVideoMaxKeyFrameIntervalKey : @(15) };
        let deviceModel = [UIDevice currentDevice].machineModel;
        NSLog(@"device model:%@",deviceModel);
        let CompatiableForH264 = [deviceModel isEqualToString:@"iPhone10,1"];
        videoSettings = @{ AVVideoCodecKey : CompatiableForH264 ? AVVideoCodecTypeHEVC : AVVideoCodecTypeH264,
                           AVVideoWidthKey : @(dimensions.width),
                           AVVideoHeightKey : @(dimensions.height),
                           AVVideoCompressionPropertiesKey : compressionProperties };
    }
    
    if ( [_assetWriter canApplyOutputSettings:videoSettings forMediaType:AVMediaTypeVideo] )
    {
        _videoInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:videoSettings sourceFormatHint:videoFormatDescription];
        _videoInput.expectsMediaDataInRealTime = YES;
        _videoInput.transform = transform;
        
        if ( [_assetWriter canAddInput:_videoInput] )
        {
            [_assetWriter addInput:_videoInput];
        }
        else
        {
            if ( errorOut ) {
                *errorOut = [[self class] cannotSetupInputError];
            }
            return NO;
        }
    }
    else
    {
        if ( errorOut ) {
            *errorOut = [[self class] cannotSetupInputError];
        }
        return NO;
    }
    
    return YES;
}
+ (NSError *)cannotSetupInputError
{
    NSString *localizedDescription = NSLocalizedString( @"Recording cannot be started", nil );
    NSString *localizedFailureReason = NSLocalizedString( @"Cannot setup asset writer input.", nil );
    NSDictionary *errorDict = @{ NSLocalizedDescriptionKey : localizedDescription,
                                 NSLocalizedFailureReasonErrorKey : localizedFailureReason };
    return [NSError errorWithDomain:@"com.apple.dts.samplecode" code:0 userInfo:errorDict];
}
- (void)teardownAssetWriterAndInputs
{
    [_videoInput release];
    _videoInput = nil;
    [_audioInput release];
    _audioInput = nil;
    [_assetWriter release];
    _assetWriter = nil;
}
@end


/*
     File: MovieRecorder.h
 Abstract: Real-time movie recorder which is totally non-blocking
  Version: 2.1
 
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
 Inc. ("Apple") in consideration of your agreement to the following
 terms, and your use, installation, modification or redistribution of
 this Apple software constitutes acceptance of these terms.  If you do
 not agree with these terms, please do not use, install, modify or
 redistribute this Apple software.
 
 In consideration of your agreement to abide by the following terms, and
 subject to these terms, Apple grants you a personal, non-exclusive
 license, under Apple's copyrights in this original Apple software (the
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
 Software, with or without modifications, in source and/or binary forms;
 provided that if you redistribute the Apple Software in its entirety and
 without modifications, you must retain this notice and the following
 text and disclaimers in all such redistributions of the Apple Software.
 Neither the name, trademarks, service marks or logos of Apple Inc. may
 be used to endorse or promote products derived from the Apple Software
 without specific prior written permission from Apple.  Except as
 expressly stated in this notice, no other rights or licenses, express or
 implied, are granted by Apple herein, including but not limited to any
 patent rights that may be infringed by your derivative works or by other
 works in which the Apple Software may be incorporated.
 
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
 Copyright (C) 2014 Apple Inc. All Rights Reserved.
 
 */


#import <Foundation/Foundation.h>

#import <CoreMedia/CMFormatDescription.h>
#import <CoreMedia/CMSampleBuffer.h>

@protocol MovieRecorderDelegate;



@interface MovieRecorder : NSObject

- (instancetype)initWithURL:(NSURL *)URL;

// Only one audio and video track each are allowed.
- (void)addVideoTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription transform:(CGAffineTransform)transform settings:(NSDictionary *)videoSettings; // see AVVideoSettings.h for settings keys/values
- (void)addAudioTrackWithSourceFormatDescription:(CMFormatDescriptionRef)formatDescription settings:(NSDictionary *)audioSettings; // see AVAudioSettings.h for settings keys/values

- (void)setDelegate:(id<MovieRecorderDelegate>)delegate callbackQueue:(dispatch_queue_t)delegateCallbackQueue; // delegate is weak referenced

- (void)prepareToRecord; // Asynchronous, might take several hundred milliseconds. When finished the delegate's recorderDidFinishPreparing: or recorder:didFailWithError: method will be called.

- (void)appendVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer;
- (void)appendVideoPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime;
- (void)appendAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer;

- (void)finishRecording; // Asynchronous, might take several hundred milliseconds. When finished the delegate's recorderDidFinishRecording: or recorder:didFailWithError: method will be called.

@end

@protocol MovieRecorderDelegate <NSObject>
@required
- (void)movieRecorderDidFinishPreparing:(MovieRecorder *)recorder;
- (void)movieRecorder:(MovieRecorder *)recorder didFailWithError:(NSError *)error;
- (void)movieRecorderDidFinishRecording:(MovieRecorder *)recorder;
@end

//
//  APSampleBufferWarpper.m
//  arpha plus
//
//  Created by michael on 2023/4/6.
//

#import "APSampleBufferWrapper.h"
#import <AVFoundation/AVFoundation.h>


@implementation APSampleBufferWrapper

+ (CMSampleBufferRef)wrapWithYUVData:(NSData *)data size:(CGSize)size time:(CMSampleTimingInfo)timingInfo; {
    unsigned char*yuvData = (unsigned char*)data.bytes;
    int width = (int)size.width;
    int height = (int)size.height;
    CVReturn ret;
    CVPixelBufferRef pixelBuffer = NULL;
    OSType pixelFormatType = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange;

    // 使用CVPixelBufferCreate函数创建CVPixelBufferRef对象
    ret = CVPixelBufferCreate(NULL, width, height, pixelFormatType, NULL, &pixelBuffer);
    if (ret != kCVReturnSuccess) {
        NSLog(@"Failed to create pixel buffer");
        return nil;
    }

    // 锁定CVPixelBufferRef对象的基地址
    CVPixelBufferLockBaseAddress(pixelBuffer, 0);

    // 将YUV数据复制到CVPixelBufferRef对象的基地址中
    unsigned char *yDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
    unsigned char *uvDestPlane = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);

    unsigned char *ySrcPlane = yuvData;
    unsigned char *uSrcPlane = yuvData + width*height;
    unsigned char *vSrcPlane = yuvData + width*height + width*height/4;

    int yDestStride = (int) CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
    int uvDestStride = (int) CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);

    int ySrcStride = width;
    int uvSrcStride = width/2;

    // 复制Y平面
    for (int i = 0; i < height; i++) {
        memcpy(yDestPlane, ySrcPlane, width);
        yDestPlane += yDestStride;
        ySrcPlane += ySrcStride;
    }

    // 复制U和V平面
    for (int i = 0; i < height/2; i++) {
        // 复制U平面
        memcpy(uvDestPlane, uSrcPlane, width/2);
        uvDestPlane += uvDestStride;
        uSrcPlane += uvSrcStride;

        // 复制V平面
        memcpy(uvDestPlane, vSrcPlane, width/2);
        uvDestPlane += uvDestStride;
        vSrcPlane += uvSrcStride;
    }

    // 解锁CVPixelBufferRef对象的基地址
    CVPixelBufferUnlockBaseAddress(pixelBuffer, 0);
    
    CMSampleBufferRef sampleBuffer = [self wrapWithPixelBuffr:pixelBuffer time:timingInfo];
    CVPixelBufferRelease(pixelBuffer);
    return sampleBuffer;
}

+ (CMSampleBufferRef)wrapWithPixelBuffr:(CVPixelBufferRef)pixelBuffer time:(CMSampleTimingInfo)timingInfo {
    CMSampleBufferRef sampleBuffer = NULL;
    CMVideoFormatDescriptionRef videoFormat;
    CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, &videoFormat);
    CMSampleBufferCreateReadyWithImageBuffer(kCFAllocatorDefault, pixelBuffer, videoFormat, &timingInfo, &sampleBuffer);
    return sampleBuffer;
}

+ (AudioStreamBasicDescription)pcmASBDWithSampleRate:(int)sampleRate channels:(int)channels bitDepth:(int)bitDepth {
    AudioStreamBasicDescription asbd = {0};
    asbd.mSampleRate = sampleRate;
    asbd.mFormatID = kAudioFormatLinearPCM;
    asbd.mFormatFlags = kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
    asbd.mChannelsPerFrame = channels;
    asbd.mBitsPerChannel = bitDepth;
    asbd.mFramesPerPacket = 1;
    asbd.mBytesPerFrame = asbd.mBitsPerChannel / 8 * asbd.mChannelsPerFrame;
    asbd.mBytesPerPacket = asbd.mBytesPerFrame * asbd.mFramesPerPacket;
    return asbd;
}
/// 音频流格式数据描述
/// - Parameters:
///   - formatID: 格式标识，如pcm、aac、g711u等
///   - formatFlags: 格式存储标识，如kAudioFormatFlagIsSignedInteger、数据以有符号整型数据存储
///   - sampleRate: 采样率，如8000、22400等
///   - channels: 声道数
///   - framesPerPacket: 每个音频包的帧数
AudioStreamBasicDescription ASBD(AudioFormatID ID, AudioFormatFlags flags, float sampleRate, int channels, int framesPerPacket) {
    BOOL isPCM = ID == kAudioFormatLinearPCM;
    AudioStreamBasicDescription asbd = {0};
    asbd.mSampleRate = sampleRate;
    asbd.mFormatID = ID;
    asbd.mFormatFlags = flags;
    asbd.mChannelsPerFrame = channels;
    asbd.mBitsPerChannel = isPCM?16:0;
    asbd.mFramesPerPacket = isPCM?1:framesPerPacket;
    asbd.mBytesPerFrame = isPCM?(asbd.mBitsPerChannel / 8 * asbd.mChannelsPerFrame):0;
    asbd.mBytesPerPacket = isPCM?(asbd.mBytesPerFrame * asbd.mFramesPerPacket):0;
    return asbd;
}

+ (CMSampleBufferRef)wrapAudioSampleBufferWithData:(NSData *)data format:(CMAudioFormatDescriptionRef)format {
    const AudioStreamBasicDescription *asbd = CMAudioFormatDescriptionGetStreamBasicDescription(format);
    CMSampleBufferRef sampleBuffer = NULL;
    AudioBufferList audioBufferList;
    audioBufferList.mNumberBuffers = 1;
    audioBufferList.mBuffers[0].mNumberChannels = asbd->mChannelsPerFrame;
    audioBufferList.mBuffers[0].mDataByteSize= (uint32_t)data.length;
    audioBufferList.mBuffers[0].mData = (void *)data.bytes;
    
    // 设置CMSampleTimingInfo结构体
    CMSampleTimingInfo timingInfo;
    timingInfo.duration = CMTimeMake(asbd->mFramesPerPacket, asbd->mSampleRate);
    timingInfo.presentationTimeStamp = CMTimeMake(data.length/asbd->mBytesPerFrame, asbd->mSampleRate);
    timingInfo.decodeTimeStamp = kCMTimeInvalid;

    OSStatus status = CMSampleBufferCreate(kCFAllocatorDefault, NULL, false, NULL, NULL, format, data.length/asbd->mBytesPerFrame, 1, &timingInfo, 0, NULL, &sampleBuffer);
    if (status != noErr) {
        NSLog(@"CMSampleBufferRef create failed");
        return NULL;
    }
    
    status = CMSampleBufferSetDataBufferFromAudioBufferList(sampleBuffer, kCFAllocatorDefault, kCFAllocatorDefault, 0, &audioBufferList);
    if (status != noErr) {
        NSLog(@"CMSampleBufferSetDataBufferFromAudioBufferList failed");
        return NULL;
    }
    return sampleBuffer;
}

+ (CMSampleBufferRef)wrapAudioData:(NSData *)data asbd:(AudioStreamBasicDescription)asbd {
    CMSampleBufferRef sampleBuffer = NULL;
    CMAudioFormatDescriptionRef formatDescription = NULL;
    AudioBufferList audioBufferList;
    audioBufferList.mNumberBuffers = 1;
    audioBufferList.mBuffers[0].mNumberChannels = asbd.mChannelsPerFrame;
    audioBufferList.mBuffers[0].mDataByteSize= (uint32_t)data.length;
    audioBufferList.mBuffers[0].mData = (void *)data.bytes;
    
    OSStatus status = noErr;
    status = CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &asbd, 0, NULL, 0, NULL, NULL, &formatDescription);
    if (status != noErr) {
        NSLog(@"CMAudioFormatDescriptionRef create failed");
        return NULL;
    }
    // 设置CMSampleTimingInfo结构体
    CMSampleTimingInfo timingInfo;
    timingInfo.duration = CMTimeMake(asbd.mFramesPerPacket, asbd.mSampleRate);
    timingInfo.presentationTimeStamp = CMTimeMake(data.length/asbd.mBytesPerFrame, asbd.mSampleRate);
    timingInfo.decodeTimeStamp = kCMTimeInvalid;

    status = CMSampleBufferCreate(kCFAllocatorDefault, NULL, false, NULL, NULL, formatDescription, data.length/asbd.mBytesPerFrame, 1, &timingInfo, 0, NULL, &sampleBuffer);
    if (status != noErr) {
        NSLog(@"CMSampleBufferRef create failed");
        return NULL;
    }
    
    status = CMSampleBufferSetDataBufferFromAudioBufferList(sampleBuffer, kCFAllocatorDefault, kCFAllocatorDefault, 0, &audioBufferList);
    if (status != noErr) {
        NSLog(@"CMSampleBufferSetDataBufferFromAudioBufferList failed");
        return NULL;
    }
    return sampleBuffer;
}

+ (CMSampleBufferRef)wrapWithPCMData:(NSData *)data {
    return [self wrapAudioData:data asbd:ASBD(kAudioFormatLinearPCM, kAudioFormatFlagIsSignedInteger|kAudioFormatFlagIsPacked, 8000, 1, 1)];
}

- (NSData *)unwrapSampleBufferToPCM:(CMSampleBufferRef)sampleBuffer {
    AudioBufferList audioBufferList;
    OSStatus status = CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer(sampleBuffer, NULL, &audioBufferList, sizeof(audioBufferList), NULL, NULL, kCMSampleBufferFlag_AudioBufferList_Assure16ByteAlignment, NULL);
    if (status != noErr) {
        NSLog(@"CMSampleBufferGetAudioBufferListWithRetainedBlockBuffer failed");
        return nil;
    }
    //采样数量
    CMItemCount numSamplesBuffer = CMSampleBufferGetNumSamples(sampleBuffer);
    //采样位深为16bit，因此数据长度计算时需要X2
    return [NSData dataWithBytes:audioBufferList.mBuffers[0].mData length:numSamplesBuffer*sizeof(short)];
}


+ (CMSampleBufferRef)wrapWithImage:(UIImage *)image time:(CMSampleTimingInfo)timingInfo {
    CVPixelBufferRef pixelBuffer = [self pixelBufferForImage:image];
    CMSampleBufferRef sampleBuffer = [self wrapWithPixelBuffr:pixelBuffer time:timingInfo];
    CVPixelBufferRelease(pixelBuffer);
    return sampleBuffer;
}


+ (CVPixelBufferRef)pixelBufferForImage:(UIImage *)image {
    CGSize size = image.size;
    NSDictionary *options =[NSDictionary dictionaryWithObjectsAndKeys:
                            @YES,kCVPixelBufferCGImageCompatibilityKey,
                            @YES,kCVPixelBufferCGBitmapContextCompatibilityKey,nil];
    CVPixelBufferRef pxbuffer =NULL;
    CVReturn status = CVPixelBufferCreate(kCFAllocatorDefault,size.width,size.height,kCVPixelFormatType_32ARGB,(__bridge CFDictionaryRef) options,&pxbuffer);
    
    NSParameterAssert(status ==kCVReturnSuccess && pxbuffer !=NULL);
    
    CVPixelBufferLockBaseAddress(pxbuffer,0);
    
    void *pxdata =CVPixelBufferGetBaseAddress(pxbuffer);
    NSParameterAssert(pxdata !=NULL);
    CGColorSpaceRef rgbColorSpace=CGColorSpaceCreateDeviceRGB();
    CGContextRef context =CGBitmapContextCreate(pxdata,size.width,size.height,8,4*size.width,rgbColorSpace,kCGImageAlphaPremultipliedFirst);
    NSParameterAssert(context);

    CGContextDrawImage(context,CGRectMake(0,0,CGImageGetWidth(image.CGImage),CGImageGetHeight(image.CGImage)), image.CGImage);
    // 释放色彩空间
    CGColorSpaceRelease(rgbColorSpace);
    // 释放context
    CGContextRelease(context);
    // 解锁pixel buffer
    CVPixelBufferUnlockBaseAddress(pxbuffer,0);
    
    return pxbuffer;
}

//+ (CVPixelBufferRef)pixelBufferForAVFrame:(AVFrame *)frame {
//    // 创建CVPixelBuffer对象
//    CVPixelBufferRef pixelBuffer = NULL;
//    CVReturn result = CVPixelBufferCreateWithPlanarBytes(NULL, frame->width, frame->height, kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, NULL, frame->linesize[0], frame->data[0], frame->linesize[1], frame->data[1], frame->linesize[2], frame->data[2], NULL, NULL, NULL, &pixelBuffer);
//    if (result != kCVReturnSuccess) {
//        // 创建失败的处理
//        return NULL;
//    }
//
//    // 释放资源
//    return pixelBuffer;
//}

@end

//
//  APSampleBufferWarpper.h
//  arpha plus
//
//  Created by michael on 2023/4/6.
//

#import <UIKit/UIKit.h>
#import <CoreMedia/CoreMedia.h>

NS_ASSUME_NONNULL_BEGIN

@interface APSampleBufferWrapper : NSObject

AudioStreamBasicDescription ASBD(AudioFormatID ID, AudioFormatFlags flags, float sampleRate, int channels, int framesPerPacket);

+ (CMSampleBufferRef)wrapWithYUVData:(NSData *)data size:(CGSize)size time:(CMSampleTimingInfo)timingInfo;

+ (CMSampleBufferRef)wrapWithPixelBuffr:(CVPixelBufferRef)pixelBuffer time:(CMSampleTimingInfo)timingInfo;

+ (CMSampleBufferRef)wrapAudioData:(NSData *)data asbd:(AudioStreamBasicDescription)asbd;

+ (CMSampleBufferRef)wrapAudioSampleBufferWithData:(NSData *)data format:(CMAudioFormatDescriptionRef)format;

+ (CMSampleBufferRef)wrapWithPCMData:(NSData *)data;

+ (CMSampleBufferRef)wrapWithImage:(UIImage *)image time:(CMSampleTimingInfo)timingInfo;

+ (CVPixelBufferRef)pixelBufferForImage:(UIImage *)image;

//+ (CVPixelBufferRef)pixelBufferForAVFrame:(AVFrame *)frame;

@end

NS_ASSUME_NONNULL_END

//
//  APVideoRecorder.m
//  arpha plus
//
//  Created by michael on 2023/8/31.
//

#import "APVideoRecorder.h"
#import "VideoRecorder.h"
#import "APSampleBufferWrapper.h"

@interface APVideoRecorder()<VideoRecorderDelegate>

@property (nonatomic, strong) VideoRecorder *writer;
@property (nonatomic, assign) BOOL isRecording;
@property (nonatomic, assign) CMAudioFormatDescriptionRef audioFormatDesc;
@property (nonatomic, assign) CMVideoFormatDescriptionRef videoFormatDesc;
@property (nonatomic,   copy) void(^completionBlock)(BOOL success);
@property (nonatomic,   copy) void(^startBlock)(void);
@property (nonatomic, assign) NSTimeInterval timestamp;

@end

@implementation APVideoRecorder

- (CMAudioFormatDescriptionRef)audioFormatDesc {
    if (!_audioFormatDesc) {
        let asbd = ASBD(kAudioFormatULaw, kAudioFormatFlagIsSignedInteger|kAudioFormatFlagIsPacked, 8000, 1, 160);
        CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &asbd, 0, NULL, 0, NULL, NULL, &_audioFormatDesc);
    }
    return _audioFormatDesc;
}

- (VideoRecorder *)writer {
    if (!_writer) {
        _writer = [[VideoRecorder alloc] initWithURL:[NSURL fileURLWithPath:_filePath]];
        [_writer addAudioTrackWithSourceFormatDescription:self.audioFormatDesc settings:nil];
//        [_writer addVideoTrackWithSourceFormatDescription:self.videoFormatDesc transform:CGAffineTransformMakeRotation(M_PI_2 * 3) settings:nil];
        [_writer setDelegate:self callbackQueue:dispatch_queue_create( "com.apple.sample.capturepipeline.recordercallback", DISPATCH_QUEUE_SERIAL)];
    }
    return _writer;
}



- (void)startWriteWithFilePath:(NSString *)filePath startBlock:(nonnull void (^)(void))startBlock {
    _filePath = filePath;
    _startBlock = startBlock;
    _timestamp = 0;
    _isRecording = NO;
    [self.writer prepareToRecord];
}

- (void)stopWriteWithCompletion:(void (^)(BOOL))completion {
    _completionBlock = completion;
    [_writer finishRecording];
    _isRecording = NO;
    _timestamp = 0;
}

- (void)writeAudioBuffer:(NSData *)audioBuffer {
    if (!self.isRecording) return;
    CMSampleBufferRef sampleBuffer = [APSampleBufferWrapper wrapAudioSampleBufferWithData:audioBuffer format:self.audioFormatDesc];
    [self.writer appendAudioSampleBuffer:sampleBuffer];
    CFSafeRelease(sampleBuffer);
}

- (void)writeVideoBuffer:(CMSampleBufferRef)videoBuffer {
    if (!self.isRecording) return;
    [self.writer appendVideoSampleBuffer:videoBuffer];
}

NS_INLINE void CFSafeRelease(CFTypeRef cf) {
    if (cf && CFGetRetainCount(cf) > 0) {
        CFRelease(cf);
        cf = NULL;
    }
}

#pragma mark - VideoRecorderDelegate

- (void)videoRecorderDidFinishPreparing:(VideoRecorder *)recorder {
    NSLog(@"recordTag: record prepared");
    if (_startBlock) _startBlock();
    _isRecording = YES;
}

- (void)videoRecorderDidFinishRecording:(VideoRecorder *)recorder {
    NSLog(@"recordTag: record finished");
    if (_completionBlock) _completionBlock(YES);
    _isRecording = NO;
}

- (void)videoRecorder:(VideoRecorder *)recorder didFailWithError:(NSError *)error {
    NSLog(@"recordTag: record failed:%@",error.description);
    if (_completionBlock) _completionBlock(NO);
    _isRecording = NO;
    _filePath = nil;
}


- (void)dealloc {
    CFSafeRelease(_audioFormatDesc);
    CFSafeRelease(_videoFormatDesc);
    APMethodLog
}

@end

//
//  APVideoRecorder.h
//  arpha plus
//
//  Created by michael on 2023/8/31.
//

#import <Foundation/Foundation.h>
#import <CoreMedia/CMSampleBuffer.h>

NS_ASSUME_NONNULL_BEGIN

@interface APVideoRecorder : NSObject

@property (nonatomic, assign, readonly) BOOL isRecording;
@property (nonatomic,   copy, readonly) NSString *filePath;

- (void)startWriteWithFilePath:(NSString *)filePath startBlock:(void(^)(void))startBlock;

- (void)writeAudioBuffer:(NSData *)audioBuffer;

- (void)writeVideoBuffer:(CMSampleBufferRef)videoBuffer;

- (void)stopWriteWithCompletion:(void(^)(BOOL success))completion;


@end

NS_ASSUME_NONNULL_END

//
//  APMovieRecorder.m
//  APP2PDemo
//
//  Created by michael on 2023/4/22.
//

#import "APMovieRecorder.h"
#import "MovieRecorder.h"
#import "APSampleBufferWrapper.h"

@interface APMovieRecorder()<MovieRecorderDelegate>

@property (nonatomic, strong) MovieRecorder *writer;
@property (nonatomic, assign) CMAudioFormatDescriptionRef audioFormatDesc;
@property (nonatomic, assign) CMAudioFormatDescriptionRef lawUFormat;

@property (nonatomic, assign) CMVideoFormatDescriptionRef h265Format;
@property (nonatomic,   copy) void(^completionBlock)(BOOL success);
@property (nonatomic,   copy) void(^startBlock)(void);
@property (nonatomic, assign) NSTimeInterval timestamp;
@property (nonatomic, assign) CGSize dimensions;
@property (nonatomic, assign) CGFloat rotation;
@property (nonatomic, assign) NSUInteger extensionBytesPerRow;

@end

#define NSStr(cfs)  (__bridge NSString *)cfs
@implementation APMovieRecorder

- (NSUInteger)extensionBytesPerRow {
    if (CGSizeEqualToSize(CGSizeMake(2048, 1536), self.dimensions)) return 3076;
    return 3940;
}

- (instancetype)initWithDimensions:(CGSize)dimensions rotation:(CGFloat)rotation {
    if (self = [super init]) {
        self.dimensions = dimensions;
        self.rotation = rotation;
    }
    return self;
}

- (CMAudioFormatDescriptionRef)lawUFormat {
    if (!_lawUFormat) {
        let asbd = ASBD(kAudioFormatULaw, kAudioFormatFlagIsSignedInteger|kAudioFormatFlagIsPacked, 8000, 1, 160);
        CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &asbd, 0, NULL, 0, NULL, NULL, &_audioFormatDesc);
    }
    return _lawUFormat;
}

- (CMAudioFormatDescriptionRef)audioFormatDesc {
    if (!_audioFormatDesc) {
        let asbd = ASBD(kAudioFormatLinearPCM, kAudioFormatFlagIsSignedInteger|kAudioFormatFlagIsPacked, 8000, 1, 1);
        CMAudioFormatDescriptionCreate(kCFAllocatorDefault, &asbd, 0, NULL, 0, NULL, NULL, &_audioFormatDesc);
        NSLog(@"recordTag: audio format:%@",_audioFormatDesc);
    }
    return _audioFormatDesc;
}
/*
 {
     mediaType:'vide'
     mediaSubType:'420f'
     mediaSpecific: {
         codecType: '420f'        dimensions: 2048 x 1536
     }
     extensions: {{
     CVBytesPerRow = 3076;
     CVFieldCount = 1;
     Version = 2;
 }}
 }
}}
 */
- (CMVideoFormatDescriptionRef)videoFormatDesc {
    if (!_videoFormatDesc) {
        NSDictionary *extensions = @{NSStr(kCMFormatDescriptionExtension_BytesPerRow):@(self.extensionBytesPerRow),
                                     NSStr(kCMFormatDescriptionExtension_FieldCount):@1,
                                     NSStr(kCMFormatDescriptionExtension_ChromaLocationBottomField):NSStr(kCMFormatDescriptionChromaLocation_Left),
                                     NSStr(kCMFormatDescriptionExtension_ChromaLocationTopField):NSStr(kCMFormatDescriptionChromaLocation_Left),
                                     NSStr(kCMFormatDescriptionExtension_ColorPrimaries):NSStr(kCMFormatDescriptionColorPrimaries_ITU_R_709_2),
                                     NSStr(kCMFormatDescriptionExtension_TransferFunction):NSStr(kCMFormatDescriptionTransferFunction_ITU_R_709_2),
                                     NSStr(kCMFormatDescriptionExtension_YCbCrMatrix):NSStr(kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2),
                                     NSStr(kCMFormatDescriptionExtension_Version):@2,
        };
        CMVideoFormatDescriptionCreate(NULL, '420f', (int32_t)self.dimensions.width, (int32_t)self.dimensions.height, (__bridge CFDictionaryRef)extensions, &_videoFormatDesc);
        NSLog(@"recordTag: video format:%@",_videoFormatDesc);
    }
    return _videoFormatDesc;
}

/*
 {
     mediaType:'vide'
     mediaSubType:'hvc1'
     mediaSpecific: {
         codecType: 'hvc1'        dimensions: 2048 x 1536
     }
     extensions: {{
     BitsPerComponent = 8;
     CVFieldCount = 1;
     CVImageBufferChromaLocationBottomField = Left;
     CVImageBufferChromaLocationTopField = Left;
     CVImageBufferColorPrimaries = "ITU_R_709_2";
     CVImageBufferTransferFunction = "ITU_R_709_2";
     CVImageBufferYCbCrMatrix = "ITU_R_709_2";
     FullRangeVideo = 0;
     SampleDescriptionExtensionAtoms =     {
         hvcC = {length = 114, bytes = 0x01214000 00009000 00000000 96f000fc ... 074401c0 f7c0e6d9 };
     };
 }}
 }
 */
- (CMVideoFormatDescriptionRef)h265Format {
    if (!_h265Format) {
        NSDictionary *extensions = @{/*NSStr(kCMFormatDescriptionExtension_BitsPerComponent):@8,*/
                                     NSStr(kCMFormatDescriptionExtension_FieldCount):@1,
                                     NSStr(kCMFormatDescriptionExtension_ChromaLocationBottomField):NSStr(kCMFormatDescriptionChromaLocation_Left),
                                     NSStr(kCMFormatDescriptionExtension_ChromaLocationTopField):NSStr(kCMFormatDescriptionChromaLocation_Left),
                                     NSStr(kCMFormatDescriptionExtension_ColorPrimaries):NSStr(kCMFormatDescriptionColorPrimaries_ITU_R_709_2),
                                     NSStr(kCMFormatDescriptionExtension_TransferFunction):NSStr(kCMFormatDescriptionTransferFunction_ITU_R_709_2),
                                     NSStr(kCMFormatDescriptionExtension_YCbCrMatrix):NSStr(kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2),
                                     NSStr(kCMFormatDescriptionExtension_FullRangeVideo):@0,
                                     /// TODO:
        };
        CMVideoFormatDescriptionCreate(NULL, 'hvc1', (int32_t)self.dimensions.width, (int32_t)self.dimensions.height, (__bridge CFDictionaryRef)extensions, &_h265Format);
        NSLog(@"recordTag: video format:%@",_h265Format);
    }
    return _h265Format;
}

//TODO: startBlock增加成功和失败的结果回调
- (void)startWriteWithFilePath:(NSString *)filePath startBlock:(nonnull void (^)(void))startBlock {
    _filePath = filePath;
    _writer = [[MovieRecorder alloc] initWithURL:[NSURL fileURLWithPath:filePath]];
    [_writer addAudioTrackWithSourceFormatDescription:self.audioFormatDesc settings:nil];
    [_writer addVideoTrackWithSourceFormatDescription:self.videoFormatDesc transform:CGAffineTransformMakeRotation(DegreesToRadians(_rotation)) settings:nil];
    [_writer setDelegate:self callbackQueue:dispatch_queue_create( "com.apple.sample.capturepipeline.recordercallback", DISPATCH_QUEUE_SERIAL)];
    [_writer prepareToRecord];
    _startBlock = startBlock;
    _timestamp = 0;
    _isRecording = NO;
}


- (void)setVideoFormat:(CVPixelBufferRef)pixelBuffer {
//    if (NULL == _videoFormatDesc) {
//        CMVideoFormatDescriptionCreateForImageBuffer(kCFAllocatorDefault, pixelBuffer, &_videoFormatDesc);
//        NSLog(@"recordTag: video format:%@",_videoFormatDesc);
//    }
    
}

- (void)stopWriteWithCompletion:(void (^)(BOOL))completion {
    _completionBlock = completion;
    [_writer finishRecording];
    _writer = nil;
    _isRecording = NO;
    _timestamp = 0;
}


- (void)writePCMBuffer:(NSData *)pcmData {
    if (!_isRecording) return;
    CMSampleBufferRef sampleBuffer = [APSampleBufferWrapper wrapWithPCMData:pcmData];
    [self.writer appendAudioSampleBuffer:sampleBuffer];
    CFSafeRelease(sampleBuffer);
}


- (void)writeAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    if (!_isRecording) return;
    [self.writer appendAudioSampleBuffer:sampleBuffer];
    CFSafeRelease(sampleBuffer);
}

- (void)writePixelBuffer:(CVPixelBufferRef)pixelBuffer {
    if (!_isRecording) return;
    if (self.timestamp == 0.0) {
        self.timestamp = CFAbsoluteTimeGetCurrent();
    }
    CMTime pts = CMTimeMakeWithSeconds(CFAbsoluteTimeGetCurrent() - self.timestamp, 90000);
    [self.writer appendVideoPixelBuffer:pixelBuffer withPresentationTime:pts];
}

- (void)writePixelBuffer:(CVPixelBufferRef)pixelBuffer pts:(CMTime)pts {
    if (!_isRecording) return;
    [self.writer appendVideoPixelBuffer:pixelBuffer withPresentationTime:pts];
}

- (void)writePixelBuffer:(CVPixelBufferRef)pixelBuffer ts:(uint64_t)ts {
    if (!_isRecording) return;
    if (self.timestamp == 0) {
        self.timestamp = ts;
    }
    CMTime pts = CMTimeMakeWithSeconds((ts - self.timestamp)/1000.f, 90000);
    [self.writer appendVideoPixelBuffer:pixelBuffer withPresentationTime:pts];
}

- (void)writeVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer {
    if (!_isRecording) return;
    [self.writer appendVideoSampleBuffer:sampleBuffer];
    CFSafeRelease(sampleBuffer);
}


NS_INLINE void CFSafeRelease(CFTypeRef cf) {
    if (cf && CFGetRetainCount(cf) > 0) {
        CFRelease(cf);
        cf = NULL;
    }
}

#pragma mark - MovieRecorderDelegate

- (void)movieRecorderDidFinishPreparing:(MovieRecorder *)recorder {
    if (_startBlock) _startBlock();
    _isRecording = YES;
}

- (void)movieRecorderDidFinishRecording:(MovieRecorder *)recorder {
    if (_completionBlock) _completionBlock(YES);
}

- (void)movieRecorder:(MovieRecorder *)recorder didFailWithError:(NSError *)error {
    NSLog(@"record error:%@",[error.userInfo description]);
    if (_completionBlock) _completionBlock(NO);
    _filePath = nil;
}


- (void)dealloc {
    CFSafeRelease(_audioFormatDesc);
    CFSafeRelease(_videoFormatDesc);
    APMethodLog
}

@end

//
//  APMovieRecorder.h
//  APP2PDemo
//
//  Created by michael on 2023/4/22.
//

#import <Foundation/Foundation.h>
#import <CoreMedia/CMSampleBuffer.h>

NS_ASSUME_NONNULL_BEGIN

@interface APMovieRecorder : NSObject

@property (nonatomic, assign, readonly) BOOL isRecording;
@property (nonatomic,   copy, readonly) NSString *filePath;
@property (nonatomic, assign) CMVideoFormatDescriptionRef videoFormatDesc;

- (instancetype)initWithDimensions:(CGSize)dimensions rotation:(CGFloat)rotation;

- (void)setVideoFormat:(CVPixelBufferRef)pixelBuffer;

- (void)startWriteWithFilePath:(NSString *)filePath startBlock:(void(^)(void))startBlock;

- (void)writePixelBuffer:(CVPixelBufferRef)pixelBuffer;

- (void)writePixelBuffer:(CVPixelBufferRef)pixelBuffer pts:(CMTime)pts;

- (void)writePixelBuffer:(CVPixelBufferRef)pixelBuffer ts:(uint64_t)ts;

- (void)writeVideoSampleBuffer:(CMSampleBufferRef)sampleBuffer;

- (void)writePCMBuffer:(NSData *)pcmData;

- (void)writeAudioSampleBuffer:(CMSampleBufferRef)sampleBuffer;

- (void)stopWriteWithCompletion:(void(^)(BOOL success))completion;


@end

NS_ASSUME_NONNULL_END

//
//  FWFullscreenManager.swift
//  FuweiCloud
//
//  Created by michael on 2024/5/27.
//

import Foundation
import CoreMotion

class FWMotionManager: NSObject {
    static let notification = Notification.Name("MotionManagerNotification")
    private var motion: CMMotionManager?
    private let sensitive = 0.77
    static let shared = {
        let mgr = FWMotionManager()
        mgr.motion = CMMotionManager()
        mgr.motion?.deviceMotionUpdateInterval = 1/40.0
        return mgr
    }()
    
    func startMonitor(_ isStart: Bool) {
        guard true == motion?.isDeviceMotionAvailable else { return }
        if isStart {
            motion?.startDeviceMotionUpdates(to: OperationQueue.main, withHandler: { [unowned self] device, error in
                guard let x = device?.gravity.x,let y = device?.gravity.y else { return }
                var orientation: UIInterfaceOrientation? = nil
                if fabs(x) > sensitive {
                    orientation = x < 0 ?.landscapeLeft:.landscapeRight
                }else if fabs(y) > sensitive {
                    orientation = y < 0 ?.portrait:.portraitUpsideDown
                }
                if let orientation {
                    NotificationCenter.default.post(name: Self.notification, object: orientation)
                }
            })
        }else {
            motion?.stopDeviceMotionUpdates()
        }
    }
}

class FWFullscreenManager: NSObject {
    static let shared = FWFullscreenManager()
    
    func rotate(_ isLandscape: Bool, vc: UIViewController? = nil) {
        if #available(iOS 16.0, *) {
            vc?.setNeedsUpdateOfSupportedInterfaceOrientations()
            (UIApplication.shared.connectedScenes.first as? UIWindowScene)?.requestGeometryUpdate(.iOS(interfaceOrientations: isLandscape ?.landscapeRight:.portrait))
        }else {
            UIViewController.attemptRotationToDeviceOrientation()
            UIDevice.current.setValue(isLandscape ?UIInterfaceOrientation.landscapeRight.rawValue:UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
        }
        APApplication.statusBarOrientation = isLandscape ?.landscapeRight:.portrait
    }
    static func switching(_ vc: UIViewController? = nil) {
        shared.rotate(!isLandscape, vc: vc)
    }
}

//
//  FWAppSettingURLHelper.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/10.
//

import UIKit

public enum FWAppSettingURLType: String {
    case setting = "QXBwLXByZWZzOg=="
    case wifi = "QXBwLXByZWZzOldJRkk="
    case ble = "QXBwLXByZWZzOkJsdWV0b290aA=="
    case cellular = "QXBwLXByZWZzOk1PQklMRV9EQVRBX1NFVFRJTkdTX0lE"
    case apns = "QXBwLXByZWZzOk5PVElGSUNBVElPTlNfSUQ="
    case privacy = "QXBwLXByZWZzOlByaXZhY3k="
    case appSetting = "QXBwLXByZWZzJTNBY29tLmFycGhhLmFycGhhcGx1cw=="
}

public class FWAppSettingURLHelper {
    
    public static func gotoURL(_ urlType: FWAppSettingURLType) {
        let base64String = urlType.rawValue
        guard let data = Data(base64Encoded: base64String),
              let decodedString = String(data: data, encoding: .utf8),
              let url = URL(string: decodedString) else {
            // 如果解码失败，回退到应用设置页面
            fallbackToAppSettings()
            return
        }
        
        if UIApplication.shared.canOpenURL(url) {
            UIApplication.shared.open(url, options: [:], completionHandler: nil)
        } else {
            fallbackToAppSettings(urlType: urlType)
        }
    }
    
    private static func fallbackToAppSettings(urlType: FWAppSettingURLType? = nil) {
        var fallbackURL = URL(string: UIApplication.openSettingsURLString)!
        if urlType == .apns, #available(iOS 16.0, *) {
            fallbackURL = URL(string: UIApplication.openNotificationSettingsURLString)!
        }
        UIApplication.shared.open(fallbackURL, options: [:], completionHandler: nil)
    }
}

//
//  FWBundle.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/11.
//

import Foundation
import ObjectiveC

class FWBundle: Bundle {
    
    // MARK: - Private Properties
    
    private static var _currentLanguage: String?
    private static var _languageBundle: Bundle?
    
    private static let APLanguagesKey = "APLanguageKey"
    private static var APGroupsKey: String {
        return FWBundle.appGroupsName()
    }
    
    private static var hasSwizzled = false
    
    // MARK: - Public Methods
    
    @objc public class func swizzleIfNeeded() {
        guard !hasSwizzled else { return }
        
        let originalMethod = class_getInstanceMethod(Bundle.self, #selector(Bundle.localizedString(forKey:value:table:)))
        let swizzledMethod = class_getInstanceMethod(FWBundle.self, #selector(FWBundle.localizedString(forKey:value:table:)))
        
        if let originalMethod = originalMethod, let swizzledMethod = swizzledMethod {
            method_exchangeImplementations(originalMethod, swizzledMethod)
        }
        
        object_setClass(Bundle.main, FWBundle.self)
        
        hasSwizzled = true
    }
    
    class func appGroupsName() -> String {
        let bundleName = Bundle.main.object(forInfoDictionaryKey: "CFBundleIdentifier") as? String ?? ""
        if bundleName.hasPrefix("com.arpha") {
            return "group.com.arpha.arpha"
        } else if bundleName.hasPrefix("com.lls") {
            return "group.com.lls.cldbus"
        } else {
            return "group.com.lingdu.lingdu"
        }
    }
    
    @objc override func localizedString(forKey key: String, value: String?, table tableName: String?) -> String {
        if let bundle = FWBundle.fw_mainBundle() {
            return bundle.localizedString(forKey: key, value: value, table: tableName)
        } else {
            return super.localizedString(forKey: key, value: value, table: tableName)
        }
    }
    
    class var currentLanguage: String {
        get {
            if NSClassFromString("UIApplication") != nil {
                if _currentLanguage == nil {
                    let userDefaults = UserDefaults(suiteName: APGroupsKey)
                    var lang = userDefaults?.string(forKey: APLanguagesKey)
                    lang = lang ?? Locale.preferredLanguages.first
                    _currentLanguage = containsLanguage(lang ?? "")
                    if _currentLanguage != lang {
                        userDefaults?.set(_currentLanguage, forKey: APLanguagesKey)
                    }
                }
                return _currentLanguage ?? "en"
            } else {
                let userDefaults = UserDefaults(suiteName: APGroupsKey)
                let language = userDefaults?.string(forKey: APLanguagesKey)
                return containsLanguage(language ?? "")
            }
        }
        set {
            if NSClassFromString("UIApplication") != nil {
                let lang = containsLanguage(newValue.isEmpty ? Locale.preferredLanguages.first ?? "" : newValue)
                _currentLanguage = lang
                _languageBundle = nil // Reset Bundle
                UserDefaults(suiteName: APGroupsKey)?.set(lang, forKey: APLanguagesKey)
            }
        }
    }
    
    // MARK: - Private Methods
    
    private class func containsLanguage(_ lang: String) -> String {
        for lg in ["en", "zh-Hant", "zh-Hans"] {
            if lang.contains(lg) {
                return lg
            }
        }
        return "en"
    }
    
    class func fw_mainBundle() -> Bundle? {
        if NSClassFromString("UIApplication") != nil {
            if let languageBundle = _languageBundle {
                return languageBundle
            }
            if let fwBundlePath = Bundle.main.path(forResource: "FWBundle", ofType: "bundle"),
               let fwBundle = Bundle(path: fwBundlePath),
               let languageBundlePath = fwBundle.path(forResource: currentLanguage, ofType: "lproj") {
                _languageBundle = Bundle(path: languageBundlePath)
                return _languageBundle
            }
        } else {
            if let fwBundlePath = Bundle.main.path(forResource: "FWBundle", ofType: "bundle"),
               let fwBundle = Bundle(path: fwBundlePath),
               let languageBundlePath = fwBundle.path(forResource: currentLanguage, ofType: "lproj") {
                return Bundle(path: languageBundlePath)
            }
        }
        return nil
    }
}

//
//  FWDispatchGroup.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/11.
//

import Foundation

class FWDispatchGroup {
    private let group = DispatchGroup()
    private(set) var counter = 0
    private var isInvalidated = false
    private let lock = NSLock()
    deinit {
        invalidate()
        print("FWDispatchGroup deinit")
    }

    func enter() {
        if !self.isInvalidated {
            lock.lock()
            self.counter += 1
            lock.unlock()
            self.group.enter()
        }
    }

    func leave() {
        if self.counter > 0 {
            lock.lock()
            self.counter -= 1
            self.group.leave()
            lock.unlock()
        } else {
            print("Warning: Attempting to leave a group more times than entered")
        }
    }

    func wait(timeout: DispatchTime = .distantFuture) -> DispatchTimeoutResult {
        return group.wait(timeout: timeout)
    }

    func notify(queue: DispatchQueue = .main, execute work: @escaping () -> Void) {
        group.notify(queue: queue) { [weak self] in
            guard let self = self, !self.isInvalidated else { return }
            work()
        }
    }

    func asyncWait(queue: DispatchQueue = .global(),timeout: DispatchTime = .distantFuture, execute work: @escaping (Bool) -> Void) {
        queue.async { [weak self] in
            guard let self = self, !self.isInvalidated else { return }
            let result = self.wait(timeout: timeout)
            dispatch_async_on_main_queue {
                guard !self.isInvalidated else { return }
                work(result == .timedOut)
                if result == .timedOut {
                    self.lock.lock()
                    self.counter = 0
                    self.lock.unlock()
                }
            }
        }
    }

    func invalidate() {
        lock.lock()
        self.isInvalidated = true
        while self.counter > 0 {
            self.group.leave()
            self.counter -= 1
        }
        lock.unlock()
    }
}

//
//  FWFileManager.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/11.
//

import CommonCrypto
import Compression
import Foundation
import ImageIO
import MobileCoreServices
import UIKit

/*
 FWFileManager 实现的功能包括：
 文件和目录操作（创建、复制、移动、删除等）
 文件和目录属性读取
 目录内容列举
 文件读写操作
 路径处理和标准化
 系统目录路径获取
 图像元数据读取
 扩展属性（xattr）操作
 */
public class FWFileManager {
    // MARK: - Private Properties

    private static var absoluteDirectories: [String] = {
        let fileManager = FileManager.default
        let directories = [
            fileManager.urls(for: .applicationSupportDirectory, in: .userDomainMask).first,
            fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first,
            fileManager.urls(for: .documentDirectory, in: .userDomainMask).first,
            fileManager.urls(for: .libraryDirectory, in: .userDomainMask).first,
            Bundle.main.resourceURL,
            URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
        ].compactMap { $0?.path }
        return directories.sorted { $0.count > $1.count }
    }()

    // MARK: - Public Methods

    public static func absoluteDirectoryForPath(_ path: String) -> String? {
        assertPath(path)

        if path == "/" {
            return nil
        }

        for directory in absoluteDirectories {
            if path.hasPrefix(directory) {
                return directory
            }
        }

        return nil
    }

    public static func absolutePath(_ path: String) -> String {
        assertPath(path)

        if let defaultDirectory = absoluteDirectoryForPath(path) {
            return path
        } else {
            return pathForDocumentsDirectoryWithPath(path)
        }
    }

    public static func attribute(ofItemAtPath path: String, forKey key: FileAttributeKey) -> Any? {
        return try? FileManager.default.attributesOfItem(atPath: absolutePath(path))[key]
    }

    public static func attributes(ofItemAtPath path: String) -> [FileAttributeKey: Any]? {
        return try? FileManager.default.attributesOfItem(atPath: absolutePath(path))
    }

    public static func copyItem(atPath path: String, toPath: String, overwrite: Bool = false) throws {
        let absoluteToPath = absolutePath(toPath)

        if FileManager.default.fileExists(atPath: absoluteToPath) {
            if overwrite {
                try FileManager.default.removeItem(atPath: absoluteToPath)
            } else {
                throw NSError(domain: "FWFileManagerError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Destination path already exists"])
            }
        }

        try createDirectories(forFileAtPath: absoluteToPath)
        try FileManager.default.copyItem(atPath: absolutePath(path), toPath: absoluteToPath)
    }

    public static func createDirectories(forFileAtPath path: String) throws {
        let directoryPath = (absolutePath(path) as NSString).deletingLastPathComponent
        try FileManager.default.createDirectory(atPath: directoryPath, withIntermediateDirectories: true, attributes: nil)
    }

    public static func createFile(atPath path: String, content: Any? = nil, overwrite: Bool = false) throws {
        let absolutePath = self.absolutePath(path)

        if FileManager.default.fileExists(atPath: absolutePath) {
            if overwrite {
                try FileManager.default.removeItem(atPath: absolutePath)
            } else {
                throw NSError(domain: "FWFileManagerError", code: 2, userInfo: [NSLocalizedDescriptionKey: "File already exists"])
            }
        }

        try createDirectories(forFileAtPath: absolutePath)

        if let content = content {
            try write(fileAtPath: absolutePath, content: content)
        } else {
            FileManager.default.createFile(atPath: absolutePath, contents: nil, attributes: nil)
        }
    }

    public static func creationDate(ofItemAtPath path: String) -> Date? {
        return attribute(ofItemAtPath: path, forKey: .creationDate) as? Date
    }

    public static func modificationDate(ofItemAtPath path: String) -> Date? {
        return attribute(ofItemAtPath: path, forKey: .modificationDate) as? Date
    }

    public static func emptyCachesDirectory() throws {
        try removeFilesInDirectory(atPath: pathForCachesDirectory())
    }

    public static func emptyTemporaryDirectory() throws {
        try removeFilesInDirectory(atPath: pathForTemporaryDirectory())
    }

    public static func exists(itemAtPath path: String) -> Bool {
        return FileManager.default.fileExists(atPath: absolutePath(path))
    }

    public static func isDirectory(itemAtPath path: String) -> Bool {
        var isDirectory: ObjCBool = false
        FileManager.default.fileExists(atPath: absolutePath(path), isDirectory: &isDirectory)
        return isDirectory.boolValue
    }

    public static func isFile(itemAtPath path: String) -> Bool {
        var isDirectory: ObjCBool = false
        let exists = FileManager.default.fileExists(atPath: absolutePath(path), isDirectory: &isDirectory)
        return exists && !isDirectory.boolValue
    }

    public static func isExecutable(itemAtPath path: String) -> Bool {
        return FileManager.default.isExecutableFile(atPath: absolutePath(path))
    }

    public static func isReadable(itemAtPath path: String) -> Bool {
        return FileManager.default.isReadableFile(atPath: absolutePath(path))
    }

    public static func isWritable(itemAtPath path: String) -> Bool {
        return FileManager.default.isWritableFile(atPath: absolutePath(path))
    }

    public static func listDirectories(inDirectoryAtPath path: String, deep: Bool = false) -> [String] {
        return listItems(inDirectoryAtPath: path, deep: deep).filter { isDirectory(itemAtPath: $0) }
    }

    public static func listFiles(inDirectoryAtPath path: String, deep: Bool = false) -> [String] {
        return listItems(inDirectoryAtPath: path, deep: deep).filter { isFile(itemAtPath: $0) }
    }

    public static func listItems(inDirectoryAtPath path: String, deep: Bool = false) -> [String] {
        let absolutePath = self.absolutePath(path)
        let enumerator = FileManager.default.enumerator(atPath: absolutePath)
        var items: [String] = []

        while let filePath = enumerator?.nextObject() as? String {
            let fullPath = (absolutePath as NSString).appendingPathComponent(filePath)
            items.append(fullPath)

            if !deep {
                enumerator?.skipDescendants()
            }
        }

        return items
    }

    public static func moveItem(atPath path: String, toPath: String, overwrite: Bool = false) throws {
        try copyItem(atPath: path, toPath: toPath, overwrite: overwrite)
        try removeItem(atPath: path)
    }

    public static func removeItem(atPath path: String) throws {
        try FileManager.default.removeItem(atPath: absolutePath(path))
    }

    public static func removeFilesInDirectory(atPath path: String) throws {
        let files = listFiles(inDirectoryAtPath: path)
        for file in files {
            try removeItem(atPath: file)
        }
    }

    public static func renameItem(atPath path: String, withName newName: String) throws {
        let absolutePath = self.absolutePath(path)
        let newPath = ((absolutePath as NSString).deletingLastPathComponent as NSString).appendingPathComponent(newName)
        try moveItem(atPath: absolutePath, toPath: newPath)
    }

    public static func size(ofItemAtPath path: String) -> Int64? {
        return (attribute(ofItemAtPath: path, forKey: .size) as? NSNumber)?.int64Value
    }

    public static func sizeFormatted(ofItemAtPath path: String) -> String? {
        guard let size = size(ofItemAtPath: path) else { return nil }
        return ByteCountFormatter.string(fromByteCount: size, countStyle: .file)
    }

    // MARK: - Directory Path Methods

    public static func pathForDocumentsDirectory() -> String {
        return FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first?.path ?? ""
    }

    public static func pathForDocumentsDirectoryWithPath(_ path: String) -> String {
        return (pathForDocumentsDirectory() as NSString).appendingPathComponent(path)
    }

    public static func pathForApplicationSupportDirectory() -> String {
        return FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first?.path ?? ""
    }

    public static func pathForCachesDirectory() -> String {
        return FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first?.path ?? ""
    }

    public static func pathForLibraryDirectory() -> String {
        return FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.path ?? ""
    }

    public static func pathForMainBundleDirectory() -> String {
        return Bundle.main.resourcePath ?? ""
    }

    public static func pathForTemporaryDirectory() -> String {
        return NSTemporaryDirectory()
    }

    // MARK: - Read Methods

    public static func readFileAtPath(_ path: String) -> String? {
        return try? String(contentsOfFile: absolutePath(path), encoding: .utf8)
    }

    public static func readFileAtPathAsData(_ path: String) -> Data? {
        return try? Data(contentsOf: URL(fileURLWithPath: absolutePath(path)))
    }

    public static func readFileAtPathAsImage(_ path: String) -> UIImage? {
        return UIImage(contentsOfFile: absolutePath(path))
    }

    // MARK: - Write Methods

    public static func writeFile(atPath path: String, content: Any) throws {
        try write(fileAtPath: absolutePath(path), content: content)
    }

    // MARK: - Image Metadata Methods

    public static func metadataOfImage(atPath path: String) -> [String: Any]? {
        guard let url = URL(string: absolutePath(path)),
              let source = CGImageSourceCreateWithURL(url as CFURL, nil),
              let metadata = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any]
        else {
            return nil
        }
        return metadata
    }

    public static func exifDataOfImage(atPath path: String) -> [String: Any]? {
        return metadataOfImage(atPath: path)?[kCGImagePropertyExifDictionary as String] as? [String: Any]
    }

    public static func tiffDataOfImage(atPath path: String) -> [String: Any]? {
        return metadataOfImage(atPath: path)?[kCGImagePropertyTIFFDictionary as String] as? [String: Any]
    }

    // MARK: - Extended Attribute (xattr) Methods

    public static func xattrOfItem(atPath path: String) -> [String: String] {
        let absolutePath = self.absolutePath(path)
        var dict = [String: String]()

        let bufferLength = listxattr(absolutePath, nil, 0, 0)
        guard bufferLength != -1 else { return dict }

        let buffer = UnsafeMutablePointer<Int8>.allocate(capacity: bufferLength)
        defer { buffer.deallocate() }

        let keyLength = listxattr(absolutePath, buffer, bufferLength, 0)
        guard keyLength != -1 else { return dict }

        var start = 0
        while start < keyLength {
            let key = String(cString: buffer.advanced(by: start))
            if let value = xattrOfItem(atPath: absolutePath, getValueForKey: key) {
                dict[key] = value
            }
            start += key.utf8.count + 1
        }

        return dict
    }

    public static func xattrOfItem(atPath path: String, getValueForKey key: String) -> String? {
        let absolutePath = self.absolutePath(path)
        let valueLength = getxattr(absolutePath, key, nil, 0, 0, 0)
        guard valueLength != -1 else { return nil }

        let buffer = UnsafeMutablePointer<Int8>.allocate(capacity: valueLength)
        defer { buffer.deallocate() }

        let result = getxattr(absolutePath, key, buffer, valueLength, 0, 0)
        guard result != -1 else { return nil }

        return String(cString: buffer)
    }

    public static func xattrOfItem(atPath path: String, hasValueForKey key: String) -> Bool {
        return xattrOfItem(atPath: path, getValueForKey: key) != nil
    }

    public static func xattrOfItem(atPath path: String, removeValueForKey key: String) -> Bool {
        let absolutePath = self.absolutePath(path)
        return removexattr(absolutePath, key, 0) != -1
    }

    public static func xattrOfItem(atPath path: String, setValue value: String, forKey key: String) -> Bool {
        let absolutePath = self.absolutePath(path)
        return setxattr(absolutePath, key, value, value.utf8.count, 0, 0) != -1
    }

    // MARK: - Compression Methods

    public static func compressFile(atPath path: String, to destinationPath: String) throws {
        let data = try Data(contentsOf: URL(fileURLWithPath: absolutePath(path)))
        let compressedData = data.compress(algorithm: .zlib)
        try compressedData.write(to: URL(fileURLWithPath: absolutePath(destinationPath)))
    }

    public static func decompressFile(atPath path: String, to destinationPath: String) throws {
        let data = try Data(contentsOf: URL(fileURLWithPath: absolutePath(path)))
        let decompressedData = data.decompress(algorithm: .zlib)
        try decompressedData.write(to: URL(fileURLWithPath: absolutePath(destinationPath)))
    }

    // MARK: - Hashing Methods

    public static func md5(ofFileAtPath path: String) -> String? {
        guard let data = try? Data(contentsOf: URL(fileURLWithPath: absolutePath(path))) else {
            return nil
        }
        return data.md5
    }

    public static func sha256(ofFileAtPath path: String) -> String? {
        guard let data = try? Data(contentsOf: URL(fileURLWithPath: absolutePath(path))) else {
            return nil
        }
        return data.sha256
    }

    // MARK: - File Type Detection

    public static func mimeType(ofFileAtPath path: String) -> String {
        let url = URL(fileURLWithPath: absolutePath(path))
        let pathExtension = url.pathExtension.lowercased()

        if let uti = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension as CFString, nil)?.takeRetainedValue() {
            if let mimetype = UTTypeCopyPreferredTagWithClass(uti, kUTTagClassMIMEType)?.takeRetainedValue() {
                return mimetype as String
            }
        }
        return "application/octet-stream"
    }

    // MARK: - Batch Operations

    public static func performBatchOperation(_ operation: (String) throws -> Void, onItemsAtPaths paths: [String]) throws {
        for path in paths {
            try operation(path)
        }
    }

    // MARK: - Asynchronous Operations

    public static func readFileAsync(atPath path: String, completion: @escaping (Result<String, Error>) -> Void) {
        DispatchQueue.global(qos: .userInitiated).async {
            do {
                let content = try String(contentsOfFile: absolutePath(path), encoding: .utf8)
                dispatch_async_on_main_queue {
                    completion(.success(content))
                }
            } catch {
                dispatch_async_on_main_queue {
                    completion(.failure(error))
                }
            }
        }
    }

    public static func writeFileAsync(atPath path: String, content: String, completion: @escaping (Error?) -> Void) {
        DispatchQueue.global(qos: .userInitiated).async {
            do {
                try content.write(toFile: absolutePath(path), atomically: true, encoding: .utf8)
                dispatch_async_on_main_queue {
                    completion(nil)
                }
            } catch {
                dispatch_async_on_main_queue {
                    completion(error)
                }
            }
        }
    }

    // MARK: - Private Methods

    private static func assertPath(_ path: String) {
        precondition(!path.isEmpty, "Invalid path. Path cannot be empty string.")
    }

    private static func write(fileAtPath path: String, content: Any) throws {
        switch content {
        case let array as [Any]:
            (array as NSArray).write(toFile: path, atomically: true)
        case let data as Data:
            try data.write(to: URL(fileURLWithPath: path), options: .atomic)
        case let dictionary as [String: Any]:
            (dictionary as NSDictionary).write(toFile: path, atomically: true)
        case let string as String:
            try string.write(toFile: path, atomically: true, encoding: .utf8)
        case let image as UIImage:
            try image.pngData()?.write(to: URL(fileURLWithPath: path), options: .atomic)
        case let codable as Encodable:
            let data = try JSONEncoder().encode(codable)
            try data.write(to: URL(fileURLWithPath: path), options: .atomic)
        default:
            throw NSError(domain: "FWFileManagerError", code: 3, userInfo: [NSLocalizedDescriptionKey: "Unsupported content type"])
        }
    }
}

// MARK: - Data Extensions for Compression and Hashing

extension Data {
    func compress(algorithm: Algorithm) -> Data {
        return withUnsafeBytes { (sourcePtr: UnsafeRawBufferPointer) -> Data in
            let sourceSize = count
            let destinationSize = sourceSize
            let destinationBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: destinationSize)
            defer { destinationBuffer.deallocate() }

            let compressedSize = compression_encode_buffer(
                destinationBuffer,
                destinationSize,
                sourcePtr.baseAddress!.assumingMemoryBound(to: UInt8.self),
                sourceSize,
                nil,
                algorithm.compressionAlgorithm
            )

            return Data(bytes: destinationBuffer, count: compressedSize)
        }
    }

    func decompress(algorithm: Algorithm) -> Data {
        return withUnsafeBytes { (sourcePtr: UnsafeRawBufferPointer) -> Data in
            let sourceSize = count
            let destinationSize = sourceSize * 10 // Assume a maximum compression ratio of 10
            let destinationBuffer = UnsafeMutablePointer<UInt8>.allocate(capacity: destinationSize)
            defer { destinationBuffer.deallocate() }

            let decompressedSize = compression_decode_buffer(
                destinationBuffer,
                destinationSize,
                sourcePtr.baseAddress!.assumingMemoryBound(to: UInt8.self),
                sourceSize,
                nil,
                algorithm.compressionAlgorithm
            )

            return Data(bytes: destinationBuffer, count: decompressedSize)
        }
    }

    enum Algorithm {
        case lz4
        case zlib
        case lzma
        case lzfse

        var compressionAlgorithm: compression_algorithm {
            switch self {
            case .lz4: return COMPRESSION_LZ4
            case .zlib: return COMPRESSION_ZLIB
            case .lzma: return COMPRESSION_LZMA
            case .lzfse: return COMPRESSION_LZFSE
            }
        }
    }

    var md5: String {
        var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH))
        _ = withUnsafeBytes { bytes in
            CC_MD5(bytes.baseAddress, CC_LONG(self.count), &digest)
        }
        return digest.map { String(format: "%02hhx", $0) }.joined()
    }

    var sha256: String {
        var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
        _ = withUnsafeBytes { bytes in
            CC_SHA256(bytes.baseAddress, CC_LONG(self.count), &digest)
        }
        return digest.map { String(format: "%02hhx", $0) }.joined()
    }
}

//
//  FWParamObject.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/11.
//

import Foundation

let fwParam = FWParamObject.shared
final class FWParamObject {
    static let shared = FWParamObject()
    
    private init() {}
    
    var string: String?
    var productGroup: APProductGroup?
    var product: APProduct?
    var device: FWDeviceModel?
}

//
//  FWPhotosManager.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/11.
//

import Foundation
import Photos
import UIKit

typealias FWPhotoLibraryResultBlock = (Bool) -> Void

enum FWPhotosManager {
    private static let albumName: String = Bundle.main.infoDictionary?["CFBundleName"] as? String ?? ""
    
    static func checkPhotoLibraryPermission(completion: @escaping FWPhotoLibraryResultBlock) {
        let status: PHAuthorizationStatus
        
        if #available(iOS 14.0, *) {
            status = PHPhotoLibrary.authorizationStatus(for: .readWrite)
        } else {
            status = PHPhotoLibrary.authorizationStatus()
        }
        
        switch status {
        case .authorized:
            completion(true)
        case .notDetermined:
            PHPhotoLibrary.requestAuthorization { status in
                dispatch_async_on_main_queue {
                    completion(status == .authorized)
                }
            }
        case .denied:
            showPermissionAlert(message: "The system album is not authorized. Please go to the system settings to turn on the authorization switch".local)
            completion(false)
        case .limited:
            if #available(iOS 14.0, *) {
                showPermissionAlert(message: "Allow \(albumName) to access your all photos".local)
            }
            completion(false)
        case .restricted:
            FWToastPopView.forcePop([.body: "The system album function is restricted and cannot be used".local])
            completion(false)
        @unknown default:
            completion(false)
        }
    }
    
    static func saveMediaToPhotoLibrary(url: URL, completion: @escaping FWPhotoLibraryResultBlock) {
        checkPhotoLibraryPermission { success in
            if success {
                saveMediaCheckAlbum(url: url, resultBlock: completion)
            } else {
                completion(false)
            }
        }
    }
    
    // ... existing code ...
}

// MARK: - Private Methods

private extension FWPhotosManager {
    static func showPermissionAlert(message: String) {
        FWConfirmPopView.pop(properties: [
            .header: message,
            .rightButton: "Setting".local
        ]) { userInfo in
            guard userInfo != nil else { return }
            FWAppSettingURLHelper.gotoURL(.appSetting)
        }
    }
    
    static func existAlbum() -> Bool {
        var isExist = false
        let results = PHCollection.fetchTopLevelUserCollections(with: nil)
            
        results.enumerateObjects { collection, _, stop in
            if let assetCollection = collection as? PHAssetCollection,
               assetCollection.localizedTitle == albumName
            {
                isExist = true
                stop.pointee = true
            }
        }
        return isExist
    }
    
    static func createAlbum(resultBlock: @escaping FWPhotoLibraryResultBlock) {
        PHPhotoLibrary.shared().performChanges({
            PHAssetCollectionChangeRequest.creationRequestForAssetCollection(withTitle: albumName)
        }) { success, _ in
            dispatch_async_on_main_queue {
                resultBlock(success)
            }
        }
    }
    
    static func isImage(for url: URL) -> Bool {
        let imageTypes = ["jpg", "png", "tiff", "bmp", "ico", "gif", "heic"]
        return imageTypes.contains(url.pathExtension.lowercased())
    }
    
    static func saveMediaCheckAlbum(url: URL, resultBlock: @escaping FWPhotoLibraryResultBlock) {
        if existAlbum() {
            saveMedia(url: url, resultBlock: resultBlock)
        } else {
            createAlbum { success in
                if success {
                    saveMedia(url: url, resultBlock: resultBlock)
                } else {
                    resultBlock(false)
                }
            }
        }
    }
    
    static func saveMedia(url: URL, resultBlock: @escaping FWPhotoLibraryResultBlock) {
        let collections = PHCollection.fetchTopLevelUserCollections(with: nil)
        
        PHPhotoLibrary.shared().performChanges({
            guard let collection = collections.firstObject as? PHAssetCollection,
                  collection.localizedTitle == albumName else { return }
            
            let assetChangeRequest: PHAssetChangeRequest
            if isImage(for: url) {
                assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromImage(atFileURL: url)!
            } else {
                assetChangeRequest = PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: url)!
            }
            
            let collectionChangeRequest = PHAssetCollectionChangeRequest(for: collection)
            let assetPlaceholder = assetChangeRequest.placeholderForCreatedAsset
            collectionChangeRequest?.addAssets([assetPlaceholder] as NSFastEnumeration)
        }) { success, _ in
            dispatch_async_on_main_queue {
                resultBlock(success)
            }
        }
    }
}

//
//  VisualEffectView.swift
//  VisualEffectView
//
//  Created by Lasha Efremidze on 5/26/16.
//  Copyright © 2016 Lasha Efremidze. All rights reserved.
//

import UIKit

/// VisualEffectView is a dynamic background blur view.
@objcMembers
open class VisualEffectView: UIVisualEffectView {
    
    /// Returns the instance of UIBlurEffect.
    private let blurEffect = (NSClassFromString("_UICustomBlurEffect") as! UIBlurEffect.Type).init()
    
    /**
     Tint color.
     
     The default value is nil.
     */
    open var colorTint: UIColor? {
        get {
            return sourceOver?.value(forKeyPath: "color") as? UIColor
        }
        set {
            prepareForChanges()
            sourceOver?.setValue(newValue, forKeyPath: "color")
            sourceOver?.perform(Selector(("applyRequestedEffectToView:")), with: overlayView)
            applyChanges()
            overlayView?.backgroundColor = newValue
        }
    }
    
    /**
     Tint color alpha.

     Don't use it unless `colorTint` is not nil.
     The default value is 0.0.
     */
    open var colorTintAlpha: CGFloat {
        get { return _value(forKey: .colorTintAlpha) ?? 0.0 }
        set { colorTint = colorTint?.withAlphaComponent(newValue) }
    }
    
    /**
     Blur radius.
     
     The default value is 0.0.
     */
    open var blurRadius: CGFloat {
        get {
            return gaussianBlur?.requestedValues?["inputRadius"] as? CGFloat ?? 0
        }
        set {
            prepareForChanges()
            gaussianBlur?.requestedValues?["inputRadius"] = newValue
            applyChanges()
        }
    }
    
    /**
     Scale factor.
     
     The scale factor determines how content in the view is mapped from the logical coordinate space (measured in points) to the device coordinate space (measured in pixels).
     
     The default value is 1.0.
     */
    open var scale: CGFloat {
        get { return _value(forKey: .scale) ?? 1.0 }
        set { _setValue(newValue, forKey: .scale) }
    }
    
    // MARK: - Initialization
    
    public override init(effect: UIVisualEffect?) {
        super.init(effect: effect)
        
        scale = 1
    }
    
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        scale = 1
    }
    
}

// MARK: - Helpers

private extension VisualEffectView {
    
    /// Returns the value for the key on the blurEffect.
    func _value<T>(forKey key: Key) -> T? {
        return blurEffect.value(forKeyPath: key.rawValue) as? T
    }
    
    /// Sets the value for the key on the blurEffect.
    func _setValue<T>(_ value: T?, forKey key: Key) {
        blurEffect.setValue(value, forKeyPath: key.rawValue)
    }
    
    enum Key: String {
        case colorTint, colorTintAlpha, blurRadius, scale
    }
    
}

// ["grayscaleTintLevel", "grayscaleTintAlpha", "lightenGrayscaleWithSourceOver", "colorTint", "colorTintAlpha", "colorBurnTintLevel", "colorBurnTintAlpha", "darkeningTintAlpha", "darkeningTintHue", "darkeningTintSaturation", "darkenWithSourceOver", "blurRadius", "saturationDeltaFactor", "scale", "zoom"]

//
//  UIViewEffectViewiOS14.swift
//  VisualEffectView
//
//  Created by Lasha Efremidze on 9/14/20.
//

import UIKit

extension VisualEffectView {
    convenience init(color: UIColor, alpha: CGFloat = 0.4, blurRadius: CGFloat = 3) {
        self.init()
        colorTint = color
        colorTintAlpha = alpha
        self.blurRadius = blurRadius
    }
}

extension UIVisualEffectView {
    var backdropView: UIView? {
        return subview(of: NSClassFromString("_UIVisualEffectBackdropView"))
    }
    var overlayView: UIView? {
        return subview(of: NSClassFromString("_UIVisualEffectSubview"))
    }
    var gaussianBlur: NSObject? {
        return backdropView?.value(forKey: "filters", withFilterType: "gaussianBlur")
    }
    var sourceOver: NSObject? {
        return overlayView?.value(forKey: "viewEffects", withFilterType: "sourceOver")
    }
    func prepareForChanges() {
        self.effect = UIBlurEffect(style: .light)
        gaussianBlur?.setValue(1.0, forKeyPath: "requestedScaleHint")
    }
    func applyChanges() {
        backdropView?.perform(Selector(("applyRequestedFilterEffects")))
    }
}

extension NSObject {
    var requestedValues: [String: Any]? {
        get { return value(forKeyPath: "requestedValues") as? [String: Any] }
        set { setValue(newValue, forKeyPath: "requestedValues") }
    }
    func value(forKey key: String, withFilterType filterType: String) -> NSObject? {
        return (value(forKeyPath: key) as? [NSObject])?.first { $0.value(forKeyPath: "filterType") as? String == filterType }
    }
}

private extension UIView {
    func subview(of classType: AnyClass?) -> UIView? {
        return subviews.first { type(of: $0) == classType }
    }
}

//
//  APBaseCollectionView.swift
//  FuweiCloud
//
//  Created by michael on 2024/6/6.
//

import UIKit

class APBaseCollectionView<T>: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {

    var dataArray =  [Any]() {
        didSet {
            reloadData()
        }
    }
    var isSectionCell: Bool = false
    private var modelArray: [[T]] {
        if let dataList = dataArray as? [T] {
            return isSectionCell ?dataList.map{[$0]}:[dataList]
        }
        if let dataList = dataArray as? [[T]] {
            return dataList
        }
        return []
    }
    var numOfSections: (() -> (Int))?
    var rowsOfSection: ((Int) -> (Int))?
    var cellForRows: ((Int, Int, T?) -> UICollectionViewCell)!
    var didSelectRow: ((Int, Int, T?) -> Void)?
    var sizeForRows: ((Int, Int, T?) -> (CGSize))?
//    var sizeForHeader: ((UICollectionViewLayout,Int) -> (CGSize))?
//    var sizeForFooter: ((UICollectionViewLayout,Int) -> (CGSize))?
//    var heightForHeader: CGFloat = 0.1
//
//    var heightForFooter: CGFloat = 0.1
//
//    var viewForHeaders: ((Int, Int) -> (UICollectionReusableView))?
//    var viewForFooters: ((Int, Int) -> (UICollectionReusableView))?
    
    
    convenience init(cells: [UICollectionViewCell.Type], layout: UICollectionViewLayout) {
        self.init(frame: .zero, collectionViewLayout: layout)
        backgroundColor = .clear
        showsVerticalScrollIndicator = false
        showsHorizontalScrollIndicator = false
        cells.forEach { register($0, forCellWithReuseIdentifier: $0.className()) }
        delegate = self
        dataSource = self
    }
    
    convenience init(cells: [UICollectionViewCell.Type], itemSize: CGSize, rowColumn: CGSize = .zero, sectionInsets: UIEdgeInsets = .zero, scrollDirection: UICollectionView.ScrollDirection = .vertical) {
        let layout = UICollectionViewFlowLayout()
        layout.itemSize = itemSize
        layout.scrollDirection = scrollDirection
        layout.minimumLineSpacing = rowColumn.height
        layout.minimumInteritemSpacing = rowColumn.width
        layout.sectionInset = sectionInsets
        self.init(cells: cells, layout: layout)
    }
    
    convenience init(cell: UICollectionViewCell.Type, layout: UICollectionViewLayout) {
        self.init(cells: [cell], layout: layout)
    }
    
    convenience init(cell: UICollectionViewCell.Type, itemSize: CGSize, rowColumn: CGSize = .zero, sectionInsets: UIEdgeInsets = .zero, scrollDirection: UICollectionView.ScrollDirection = .vertical) {
        self.init(cells: [cell], itemSize: itemSize, rowColumn: rowColumn, sectionInsets: sectionInsets, scrollDirection: scrollDirection)
    }
    
    func attach(on vc: APBaseViewController,_ insets: UIEdgeInsets = .zero) {
        vc.view.addSubview(self)
        snp.makeConstraints { make in
            make.top.equalTo(vc.navigationBar.snp.bottom).offset(insets.top)
            make.left.equalTo(insets.left)
            make.right.equalTo(-insets.right)
            make.bottom.equalTo(-insets.bottom)
        }
    }
    
    private func setModel(_ section: Int, _ item: Int) -> T? {
        dataArray.isEmpty ?nil:modelArray[section][item]
    }
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        rowsOfSection?(section) ?? modelArray[section].count
    }
    
    func numberOfSections(in collectionView: UICollectionView) -> Int {
        numOfSections?() ?? modelArray.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        cellForRows(indexPath.section, indexPath.item, setModel(indexPath.section, indexPath.item))
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        didSelectRow?(indexPath.section, indexPath.row, setModel(indexPath.section, indexPath.item))
    }
    
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        sizeForRows?(indexPath.section, indexPath.item, setModel(indexPath.section, indexPath.item)) ?? (collectionViewLayout as? UICollectionViewFlowLayout)?.itemSize ?? .zero
    }

//    func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
//        if kind == UICollectionView.elementKindSectionHeader {
//            viewForHeaders?(indexPath.section, indexPath.item) ?? UICollectionReusableView(frame: CGRectMake(0, 0, APScreenW, heightForHeader))
//        }else {
//            viewForFooters?(indexPath.section, indexPath.item) ?? UICollectionReusableView(frame: CGRectMake(0, 0, APScreenW, heightForFooter))
//        }
//    }
//
//    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
//        sizeForHeader?(collectionViewLayout, section) ?? CGSizeMake(APScreenW, heightForHeader)
//    }
//
//    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
//        sizeForFooter?(collectionViewLayout, section) ?? CGSizeMake(APScreenW, heightForFooter)
//    }
}

//
//  APBaseCollectionViewCell.swift
//  FuweiCloud
//
//  Created by michael on 2024/6/6.
//

import UIKit

class APBaseCollectionViewCell<T>: UICollectionViewCell {
    var eventBlock: ((Int) -> Void)?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        initContent()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func initContent() {}
    func initModel(_ model: T) {}
    var model: T! {
        didSet {
            initModel(model)
        }
    }
}

//
//  APBaseEmptyView.h
//  arpha plus
//
//  Created by michael on 2023/12/1.
//

#import <UIKit/UIKit.h>

STATIC_STR APEmptyImage = @"APEmptyImage";
STATIC_STR APEmptyTitle = @"APEmptyTitle";
STATIC_STR APEmptyButton = @"APEmptyButton";
STATIC_STR APEmptyEventBlock = @"APEmptyEventBlock";
STATIC_STR APEmptyImagePadding = @"APEmptyImagePadding";
STATIC_STR APEmptyButtonPadding = @"APEmptyButtonPadding";
STATIC_STR APEmptyContentWidth = @"APEmptyTitleWidth";

NS_ASSUME_NONNULL_BEGIN

@interface APBaseEmptyView : UIView

+ (instancetype)emptyViewWithUserInfo:(NSDictionary *)userInfo;

///< 让对象支持下标访问
- (id)objectForKeyedSubscript:(NSString *)key;
- (void)setObject:(id)object forKeyedSubscript:(NSString *)key;

@end

NS_ASSUME_NONNULL_END

//
//  APBaseEmptyView.m
//  arpha plus
//
//  Created by michael on 2023/12/1.
//

#import "APBaseEmptyView.h"

@interface APBaseEmptyView ()

@property (nonatomic, strong) NSMutableDictionary *userInfo;


@end

@implementation APBaseEmptyView


- (instancetype)initWithUserInfo:(NSDictionary *)userInfo {
    if (self = [super init]) {
        self.userInfo = [NSMutableDictionary dictionaryWithDictionary:userInfo];
        [self initSubviews];
    }
    return self;
}

+ (instancetype)emptyViewWithUserInfo:(NSDictionary *)userInfo {
    return [[self alloc] initWithUserInfo:userInfo];
}

- (void)initSubviews {
    CGFloat width = self.userInfo[APEmptyContentWidth]?[self.userInfo[APEmptyContentWidth] floatValue]:(APScreenW - 100);
    CGFloat imagePadding = self.userInfo[APEmptyImagePadding]?[self.userInfo[APEmptyImagePadding] floatValue]:10;
    CGFloat buttonPadding = self.userInfo[APEmptyButtonPadding]?[self.userInfo[APEmptyButtonPadding] floatValue]:10;
    NSMutableArray<UIView *> *subviews = [NSMutableArray array];
    APWeakSelf
    if (APISStr(self.userInfo[APEmptyImage])) {
        let imv = APImageViewNamed(self.userInfo[APEmptyImage]);
        if (!self.userInfo[APEmptyButton]) {
            [imv addTap:^(UIGestureRecognizer * _Nonnull sender, UIGestureRecognizerState state, CGPoint location) {
                AP_SAFE_BLOCK(((APVoidBlock)weakSelf.userInfo[APEmptyEventBlock]));
            }];
        }
        imv.frame = CGRectMake(0, 0, imv.image.size.width, imv.image.size.height);
        [subviews addObject:imv];
    }
    if (self.userInfo[APEmptyTitle]) {
        let label = [UILabel labelWithText:self.userInfo[APEmptyTitle] font:@15 color:APLabel2];
        label.numberOfLines = 0;
        label.textAlignment = 1;
        label.frame = CGRectMake(0, 0, width, [label textHeightForWidth:width]);
        [subviews addObject:label];
    }
    if (self.userInfo[APEmptyButton]) {
        id button = self.userInfo[APEmptyButton];
        if ([button isKindOfClass:[UIButton class]]) {
            [(UIButton *)button addEventHandler:^(id  _Nonnull target) {
                AP_SAFE_BLOCK(((APVoidBlock)weakSelf.userInfo[APEmptyEventBlock]));
            }];
            [subviews addObject:button];
        } else if (APISStr(button)||APISAStr(button)) {
            let btn = [UIButton apGradientButton:button action:nil target:nil height:50];
            [btn addEventHandler:^(id  _Nonnull target) {
                AP_SAFE_BLOCK(((APVoidBlock)weakSelf.userInfo[APEmptyEventBlock]));
            }];
            [subviews addObject:btn];
        }
    }
    CGFloat height = 0;
    if (subviews.count == 2) {
        if ([subviews any:^BOOL(id  _Nonnull obj) {
            return [obj isKindOfClass:[UIImageView class]];
        }]) {
            height += imagePadding;
        }else height += buttonPadding;
    }else if (subviews.count == 3) height += (imagePadding + buttonPadding);
    for (int i = 0; i < subviews.count; i++) {
        let view = subviews[i];
        [self addSubview:view];
        height += view.frame.size.height;
        [view mas_makeConstraints:^(MASConstraintMaker *make) {
            make.centerX.mas_equalTo(0);
            make.size.mas_equalTo(view.frame.size);
            if (i == 0) make.top.mas_equalTo(0);
            else if (i == subviews.count - 1) make.bottom.mas_equalTo(0);
            else make.top.mas_equalTo(subviews[0].mas_bottom).offset(imagePadding);
        }];
    }
    self.frame = CGRectMake(0, 0, width, height);
}

- (id)objectForKeyedSubscript:(NSString *)key {
    return self.userInfo[key];
}

- (void)setObject:(id)object forKeyedSubscript:(NSString *)key {
    self.userInfo[key] = object;
    [self removeAllSubviews];
    [self initSubviews];
    [self mas_updateConstraints:^(MASConstraintMaker *make) {
        make.height.mas_equalTo(self.frame.size.height);
    }];
}


@end

//
//  APBaseTableView.swift
//  FuweiCloud
//
//  Created by michael on 2024/5/31.
//

import UIKit

class APBaseTableView<T>: UITableView, UITableViewDelegate, UITableViewDataSource {
    var dataArray = [Any]() {
        didSet {
            reloadData()
        }
    }
    var isSectionCell: Bool = false // 多组section时, true:[[A],[B]] 每个row=1;    false: [[A,B]]
    var modelArray: [[T]] {
        if let dataList = dataArray as? [T] {
            return isSectionCell ?dataList.map{[$0]}:[dataList]
        }
        if let dataList = dataArray as? [[T]] {
            return dataList
        }
        return []
    }
    var numOfSections: (() -> (Int))?
    var rowsOfSection: ((Int) -> (Int))?
    var cellForRows: ((Int, Int, T?) -> UITableViewCell)!
    var didSelectRow: ((Int, Int, T?) -> Void)?
    var heightForRows: ((Int, Int, T?) -> (CGFloat))?
    var heightForHeader: CGFloat = 0.1 // 每组头部固定高度
    var heightForHeaders: ((Int) -> (CGFloat))?
    var heightForFooter: CGFloat = 0.1
    var heightForFooters: ((Int) -> (CGFloat))?
    
    var viewForHeaders: ((Int) -> (UIView))?
    var viewForFooters: ((Int) -> (UIView))?
    
    convenience init(cells: [UITableViewCell.Type], style: UITableView.Style = .plain, separator: UITableViewCell.SeparatorStyle = .none) {
        self.init(frame: .zero, style: style)
        separatorStyle = separator
        showsVerticalScrollIndicator = false
        backgroundColor = .clear
        cells.forEach { register($0, forCellReuseIdentifier: $0.className()) }
        if cells.isEmpty {
            register(UITableViewCell.self, forCellReuseIdentifier: NSStringFromClass(UITableViewCell.self))
        }
        delegate = self
        dataSource = self
    }
    convenience init(cell: UITableViewCell.Type, style: UITableView.Style = .plain, separator: UITableViewCell.SeparatorStyle = .none) {
        self.init(cells: [cell], style: style, separator: separator)
    }
    
    func attach(on vc: APBaseViewController,_ insets: UIEdgeInsets = .zero) {
        vc.view.addSubview(self)
        snp.makeConstraints { make in
            make.top.equalTo(vc.navigationBar.snp.bottom).offset(insets.top)
            make.left.equalTo(insets.left)
            make.right.equalTo(-insets.right)
            make.bottom.equalTo(-insets.bottom)
        }
    }
    
    private func setModel(_ section: Int, _ row: Int) -> T? {
        dataArray.isEmpty ?nil:modelArray[section][row]
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        rowsOfSection?(section) ?? modelArray[section].count
    }
    func numberOfSections(in tableView: UITableView) -> Int {
        numOfSections?() ?? modelArray.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        cellForRows(indexPath.section, indexPath.row, setModel(indexPath.section, indexPath.row))
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        didSelectRow?(indexPath.section, indexPath.row, setModel(indexPath.section, indexPath.row))
    }
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        heightForRows?(indexPath.section, indexPath.row, setModel(indexPath.section, indexPath.row)) ?? tableView.rowHeight
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        heightForHeaders?(section) ?? heightForHeader
    }
    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        heightForFooters?(section) ?? heightForFooter
    }
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        viewForHeaders?(section) ?? UIView(frame: CGRectMake(0, 0, UIScreen.main.bounds.width, heightForHeader))
    }
    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        viewForFooters?(section) ?? UIView(frame: CGRectMake(0, 0, UIScreen.main.bounds.width, heightForFooter))
    }
}

//
//  APBaseTableViewCell.swift
//  FuweiCloud
//
//  Created by michael on 2024/5/31.
//

import UIKit

class APBaseTableViewCell<T>: UITableViewCell {
    var eventBlock: ((Int) -> Void)?
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        selectionStyle = .none
        initContent()
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    func initContent() {}
    func initModel(_ model: T) {}
    var model: T! {
        didSet {
            initModel(model)
        }
    }
}


//
//  FWGlowCardView.swift
//  FuweiCloud
//
//  Created by michael on 2024/7/9.
//

import UIKit

class FWGlowCardView: UIView {

    var cornerRadius: CGFloat = 15
    var borderWidth: CGFloat = 2
    var glowColors: [UIColor] = [.clear, .themeText, .clear]
    var glowWidth: CGFloat = 100
    var glowDuration: CGFloat = 4
    private lazy var glowLayer: CAGradientLayer = {
        let layer = CAGradientLayer()
        layer.startPoint = .zero
        layer.endPoint = CGPointMake(1, 0)
        layer.colors = glowColors.map(\.cgColor)
        layer.frame = CGRectMake(0, 0, glowWidth, 1000)
        return layer
    }()
    private lazy var contentLayer: CALayer = {
        let layer = CALayer()
        layer.masksToBounds = true
        layer.cornerRadius = cornerRadius - borderWidth
        layer.backgroundColor = UIColor.systemBackground.cgColor
        return layer
    }()

    convenience init() {
        self.init(frame: .zero)
        backgroundColor = APGray1("2")
        clipsToBounds = true
        layer.cornerRadius = cornerRadius
        layer.addSublayer(glowLayer)
        layer.addSublayer(contentLayer)
    }
    
    func startAnimation(_ start: Bool) {
        glowLayer.removeAllAnimations()
        guard start else { return }
        let rotationAnim = CABasicAnimation(keyPath: "transform.rotation.z")
        rotationAnim.duration = glowDuration
        rotationAnim.repeatCount = .infinity
        rotationAnim.fromValue = 0
        rotationAnim.toValue = Float.pi*2
        rotationAnim.isRemovedOnCompletion = false
        glowLayer.add(rotationAnim, forKey: "rotationAnim")
    }
    
    override func layoutSubviews() {
        glowLayer.center = CGPointMake(frame.width / 2, frame.height / 2)
        contentLayer.frame = bounds.insetBy(dx: borderWidth, dy: borderWidth)
        print("glow frame:\(glowLayer.frame)")
        print("content frame:\(contentLayer.frame)")
    }
}

//
//  FWGradientTextLayer.swift
//  FuweiCloud
//
//  Created by michael on 2024/8/8.
//

import UIKit

class FWGradientTextLayer: CATextLayer {
    
    var uiFont: UIFont = UIFont.systemFont(ofSize: 17) {
        didSet {
            font = CGFont((uiFont.fontName as CFString))
            fontSize = uiFont.pointSize
        }
    }
    
    var textColor: UIColor = .label {
        didSet {
            foregroundColor = textColor.cgColor
        }
    }

    private static let aligments: [NSTextAlignment: CATextLayerAlignmentMode] = [.center: .center, .left: .left, .right: .right, .natural: .natural, .justified: .justified]
    private static let truncations: [NSLineBreakMode: CATextLayerTruncationMode] = [.byCharWrapping: .none, .byClipping: .none, .byWordWrapping: .none, .byTruncatingHead: .start, .byTruncatingMiddle: .middle, .byTruncatingTail: .end]
    convenience init(text: Any?, font: Any, textColor: UIColor, lines: Int = 0, aligment: NSTextAlignment = .left, lineBreak: NSLineBreakMode = .byWordWrapping) {
        self.init()
        self.contentsScale = UIScreen.main.scale
        self.string = text
        let ft = UIFont.idFont(font)
        self.font = CGFont((ft.fontName as CFString))
        self.fontSize = ft.pointSize
        foregroundColor = textColor.cgColor
        /// 文字是否换行
        self.isWrapped = lines != 1
        /// 文字对其格式
        self.alignmentMode = Self.aligments[aligment]!
        /// 文字截断模式
        self.truncationMode = Self.truncations[lineBreak]!
        /// 文字是否像素化
        self.allowsFontSubpixelQuantization = false
        
    }
    
    
    let gradientLayer = CAGradientLayer()
    
    override func layoutSublayers() {
        CATransaction.disableActions(flag: true) {
            gradientLayer.frame = frame
        }
    }
    
}

//
//  FWAlertPopView.swift
//  FuweiCloud
//
//  Created by michael on 2024/8/22.
//

import Foundation

class FWAlertPopView: FWPopView {
    private var leftButton: UIButton!
    private var rightButton: UIButton!
    private func textHeight(text: Any?, fontSize: CGFloat) -> CGFloat {
        if let text = text as? String {
            text.height(fontSize, APScreenW - 160)
        }else if let text = text as? AString {
            text.height(forWidth: APScreenW - 160)
        }else {
            0
        }
    }
    
    override func initVariable() {
        super.initVariable()
        contentWidth = APScreenW - 100
        if let userInfo = userInfo as? [AnyHashable: Any] {
            let titleHeight = textHeight(text: userInfo[FWPopProperty.header], fontSize: 17.6)
            let messageHeight = textHeight(text: userInfo[FWPopProperty.body], fontSize: 15.6)
            if titleHeight > 0 && messageHeight > 0 {
                contentHeight = 30 + titleHeight + 20 + messageHeight + 30 + 45
            }else if titleHeight > 0 || messageHeight > 0 {
                contentHeight = 30 + titleHeight + messageHeight + 30 + 45
            }else {
                contentHeight = 30 + 75
            }
        }else {
            contentHeight = 30 + 75
        }
    }
    
    override func initContentUI() {
        guard let userInfo = userInfo as? [FWPopProperty: Any] else { return }
        let title = userInfo[.header]
        let message = userInfo[.body]
        if title != nil && message != nil {
            let titleLabel = UILabel(text: title, font: 17.6, color: .label, lines: 0, aligment: .center)
            let messageLabel = UILabel(text: message, font: 15.6, color: .label, lines: 0, aligment: .center)
            contentView.addSubviews([titleLabel,messageLabel])
            titleLabel.snp.makeConstraints { make in
                make.left.equalTo(30)
                make.right.equalTo(-30)
                make.top.equalTo(30)
            }
            messageLabel.snp.makeConstraints { make in
                make.left.equalTo(30)
                make.right.equalTo(-30)
                make.top.equalTo(titleLabel.snp.bottom).offset(20)
            }
        }else if title != nil || message != nil {
            let isTitle = title != nil
            let titleLabel = UILabel(text: isTitle ? title : message, font: isTitle ?17.6:15.6, color: .label, lines: 0, aligment: .center)
            contentView.addSubviews([titleLabel])
            titleLabel.snp.makeConstraints { make in
                make.left.equalTo(30)
                make.right.equalTo(-30)
                make.top.equalTo(30)
            }
        }
        let leftTitle = userInfo[.leftButton] ?? "Cancel".local
        let rightTitle = userInfo[.rightButton] ?? "Confirm".local
        leftButton = UIButton(title: AString(leftTitle, font: 17.6, color: .themeText), bImage: UIColor.popView)
        rightButton = UIButton(title: AString(rightTitle, font: 17.6, color: .themeText), bImage: UIColor.popView)
        leftButton.addAction(.buttonAction, onTarget: self)
        rightButton.addAction(.buttonAction, onTarget: self)
        let line1 = UIView(color: .separator)
        let line2 = UIView(color: .separator)
        contentView.addSubviews([leftButton, rightButton, line1, line2])
        leftButton.snp.makeConstraints { make in
            make.left.bottom.equalTo(0)
            make.height.equalTo(45)
            make.width.equalTo(self.contentView.snp.width).dividedBy(2)
        }
        rightButton.snp.makeConstraints { make in
            make.right.bottom.equalTo(0)
            make.height.equalTo(45)
            make.width.equalTo(self.contentView.snp.width).dividedBy(2)
        }
        line1.snp.makeConstraints { make in
            make.left.right.equalTo(0);
            make.bottom.equalTo(leftButton.snp.top)
            make.height.equalTo(1/YYScreenScale())
        }
        line2.snp.makeConstraints { make in
            make.bottom.equalTo(0)
            make.height.equalTo(leftButton)
            make.width.equalTo(1/YYScreenScale())
            make.centerX.equalToSuperview()
        }
    }
    
    @objc
    private func buttonAction(_ btn: UIButton) {
        dismiss(btn == leftButton ? nil : 1)
    }
}

//
//  FWGradientLabel.swift
//  FuweiCloud
//
//  Created by michael on 2024/8/28.
//

import UIKit

class FWGradientLabel: UILabel {

    private(set) var gradientLayer: CAGradientLayer!
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        gradientLayer = CAGradientLayer()
        gradientLayer.startPoint = CGPointMake(0, 0)
        gradientLayer.endPoint = CGPointMake(1, 1)
        gradientLayer.locations = [0,1]
        gradientLayer.colors = [UIColor(red:0, green:0.46, blue:1, alpha:1.0).cgColor, UIColor(red:0.91, green:0.28, blue:0.5, alpha:1.0).cgColor]
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func didMoveToSuperview() {
        if nil != superview {
            superview?.layer.addSublayer(gradientLayer)
            gradientLayer.mask = layer
        }else {
            gradientLayer.removeFromSuperlayer()
        }
    }
    
    override func layoutSubviews() {
        gradientLayer.frame = frame
        frame = bounds
    }

}

//
//  APBaseViewController.h
//  arpha plus
//
//  Created by michael on 2023/3/24.
//

#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN

typedef NS_ENUM(NSUInteger, APViewControllerState) {
    APViewControllerState_Init,
    APViewControllerState_Appear,
    APViewControllerState_Disappear,
    APViewControllerState_Deinit,
};
@interface APBaseViewController : UIViewController

@property (nonatomic, copy, nullable) void(^eventBlock)(id _Nullable userInfo);

/// push后从stack中移除
@property (nonatomic, assign) BOOL removedAfterPush;

/// 全局设备对象
@property (nonatomic, strong) APDeviceModel *device;

/// 使用全局对象APParam.device
@property (nonatomic, assign) BOOL useDevice;
@property (nonatomic, strong, nullable) id userInfo;
@property (nonatomic, strong, readonly) UIView *navigationBar;
@property (nonatomic, strong, readonly) UIButton *returnButton;
@property (nonatomic, strong, readonly) UILabel *titleLabel;
@property (nonatomic, assign, readonly) BOOL ap_movingFromParent;
@property (nonatomic, assign, readonly) APViewControllerState viewState;
@property (nonatomic, assign) CGFloat netErrorTop; ///< 顶部距离
//传递图片名字数组
@property (nonatomic,   copy) NSArray<UIButton *> *rightItems;

///<  适配手势返回
@property (nonatomic, copy, nullable) void(^removeFromParentBlock)(void);

@property (nonatomic, strong) UIView *titleView;

+ (instancetype)controllerWithUserInfo:(nullable id)userInfo NS_SWIFT_NAME(init(_:));

///用于给子类重写
- (void)returnButtonAction; // 箭头返回
- (void)rightItemsAction:(NSInteger)index;
- (void)networkResumed;

@end

NS_ASSUME_NONNULL_END

//
//  APBaseViewController.m
//  arpha plus
//
//  Created by michael on 2023/3/24.
//

#import "APBaseViewController.h"
#import "APBaseLiveController.h"
#import "APNavigationController.h"
#ifdef ARPHA_PLUS
#import "ArphaPlus-Swift.h"
#elif CLDBUS
#import "Cldbus-Swift.h"
#else
#import "LNDU-Swift.h"
#endif

@interface APBaseViewController ()

@property (nonatomic, strong) UIView *navigationBar;
@property (nonatomic, strong) FWNetErrorView *netErrorView;
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) UIButton *returnButton;
@property (nonatomic, assign) BOOL ap_movingFromParent;
@property (nonatomic, assign) APViewControllerState viewState;
@property (nonatomic, assign) BOOL netErrorClosed;

@end

static const NSUInteger BASE_TAG = 904894;
@implementation APBaseViewController

- (void)setNetErrorTop:(CGFloat)netErrorTop {
    _netErrorTop = netErrorTop;
    [_netErrorView mas_updateConstraints:^(MASConstraintMaker *make) {
        make.top.mas_equalTo(netErrorTop);
    }];
}

+ (instancetype)controllerWithUserInfo:(id)userInfo {
    let vc = [[self alloc] init];
    vc.userInfo = userInfo;
    if (APISClass(userInfo, APDeviceModel)) {
        vc.device = userInfo;
    }
    return vc;
}

- (void)setUseDevice:(BOOL)useDevice {
    _useDevice = useDevice;
    if (useDevice) self.device = APParam.device;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"apparam product:%@, device:%@ name:%@",APParam.product, APParam.device, [self className]);
    self.view.backgroundColor = [UIColor background];
    self.fd_prefersNavigationBarHidden = YES;
    _netErrorTop = APNaviBarHeight;
    _netErrorClosed = NO;
    APMethodLog
    [self _setupNavigationBar];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    self.viewState = self.isMovingToParentViewController?APViewControllerState_Init:APViewControllerState_Appear;
    DDLogInfo(@"[%@ %@]",[self className], self.isMovingToParentViewController?@"init":@"viewWillAppear");
    if ([APKeyWindow.rootViewController isKindOfClass:[UINavigationController class]]) {
        [APKeyWindow.rootViewController setValue:@YES forKey:@"navigationBarHidden"];
    }
}

- (void)willMoveToParentViewController:(UIViewController *)parent {
    [super willMoveToParentViewController:parent];
    self.ap_movingFromParent = parent == nil;
    NSLog(@"[%@ willMoveToParentViewController:%d]",[self className],self.ap_movingFromParent);
    
}


- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    if (self.navigationController && self.navigationController.viewControllers.count > 1) {
        NSUInteger idx = [self.navigationController.viewControllers indexOfObject:self];
        if (idx != NSNotFound && (idx - 1) >= 0 && (idx - 1) < self.navigationController.viewControllers.count) {
            UIViewController *pushVC = self.navigationController.viewControllers[idx - 1];
            if ([pushVC isKindOfClass:[APBaseViewController class]]) {
                APBaseViewController *pushVC = (APBaseViewController *)self.navigationController.viewControllers[idx - 1];
                if (pushVC.removedAfterPush) {
                    NSArray<UIViewController *> *vcs = [self.navigationController.viewControllers select:^BOOL(__kindof UIViewController * _Nonnull obj) {
                        return obj != pushVC;
                    }];
                    [self.navigationController setViewControllers:vcs animated:NO];
                }
            }
        }
    }
#ifdef DEBUG
    if ([self.navigationController.viewControllers none:^BOOL(__kindof UIViewController * _Nonnull obj) {
        return [obj isKindOfClass:[APBaseLiveController class]];
    }]) {
        if ([APTools P2PCount] > 0 || [SharedP2PManager isConnected]) {
            [UIAlertController forceAlert:@"P2P Counter Error" message:nil confirmBlock:nil];
        }
    }
#endif
}


- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    self.viewState = self.ap_movingFromParent?APViewControllerState_Deinit:APViewControllerState_Disappear;
    DDLogInfo(@"[%@ %@]",[self className], self.ap_movingFromParent?@"deinit":@"viewWillDisappear");
    if (self.ap_movingFromParent) {
        if (self.transitionCoordinator.isInteractive) { /// 手势返回
            APWeakSelf
            [self.transitionCoordinator notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext>  _Nonnull context) {
                if (!context.isCancelled) { /// 手势pop成功
                    DDLogInfo(@"vcTag: remove from parent by gesture");
                    AP_SAFE_BLOCK(weakSelf.removeFromParentBlock);
                }
            }];
        }else { ///< 非手势popVC
            DDLogInfo(@"vcTag: remove from parent by pop");
            AP_SAFE_BLOCK(self.removeFromParentBlock);
        }
    }
}


- (void)setTitle:(NSString *)title {
    [super setTitle:title];
    self.titleLabel.text = title;
}

- (void)_setupNavigationBar {
    _navigationBar = [UIView viewWithColor:[UIColor background]];
    _titleLabel = [UILabel labelWithFont:@-20 color:[UIColor labelColor]];
    _titleLabel.textAlignment = NSTextAlignmentCenter;
    [_titleLabel scale:0.7 lines:2];
    _returnButton = [UIButton buttonWithImage:@"page_return_ic"];
    [_returnButton addAction:APSEL(returnButtonAction) onTarget:self];
    [_returnButton setContentHorizontalAlignment:UIControlContentHorizontalAlignmentLeft];
    [_navigationBar addSubview:_returnButton];
    self.titleView = _titleLabel;
    _netErrorView = [[FWNetErrorView alloc] init];
    _netErrorView.hidden = !(!_netErrorClosed && ![APTools netReachable]);
    [self.view addSubviews:@[_navigationBar,_netErrorView]];
    let statusBarHeight = APStatusBarHeight;
    [_returnButton mas_makeConstraints:^(MASConstraintMaker *make) {
        make.left.mas_equalTo(20);
        make.top.mas_equalTo(statusBarHeight);
        make.size.mas_equalTo(CGSizeMake(44, 44));
    }];
    [_netErrorView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.mas_equalTo(_netErrorTop);
        make.left.right.mas_equalTo(0);
        make.height.mas_equalTo(_netErrorView.fitHeight);
    }];
    CGFloat barHeight = APNaviBarHeight+((![APTools netReachable]&&!_netErrorClosed)?_netErrorView.fitHeight:0);
    [_navigationBar mas_makeConstraints:^(MASConstraintMaker *make) {
        make.top.mas_equalTo(0);
        make.left.right.mas_equalTo(0);
        make.height.mas_equalTo(barHeight);
    }];
    APWeakSelf
    [_netErrorView setCloseAction:^{
        weakSelf.netErrorClosed = YES;
        [weakSelf refreshNavigationBar];
    }];
    self.returnButton.hidden = self.navigationController == nil || self.navigationController.viewControllers.firstObject == self;
    [self addNotificationWithName:@"APRegistNetworkChangedNotification" object:nil usingBlock:^(NSNotification * _Nonnull note) {
        [weakSelf refreshNavigationBar];
    }];
}

- (void)refreshNavigationBar {
    self.netErrorView.hidden = !(!self.netErrorClosed && ![APTools netReachable]);
    CGFloat barHeight = APNaviBarHeight+((![APTools netReachable]&&!_netErrorClosed)?_netErrorView.fitHeight:0);
    [_navigationBar mas_updateConstraints:^(MASConstraintMaker *make) {
        make.height.mas_equalTo(barHeight);
    }];
}

- (void)setTitleView:(UIView *)titleView {
    if (_titleView != titleView) {
        [_titleView removeFromSuperview];
    }
    _titleView = titleView;
    [self.navigationBar addSubview:titleView];
    CGFloat h = MIN(titleView.height?:44.f, 44.f);
    CGFloat w = MIN(titleView.width?:(APScreenW-(20+44)*2), (APScreenW-(20+44)*2));
    [titleView mas_makeConstraints:^(MASConstraintMaker *make) {
        make.height.mas_equalTo(h);
        make.width.mas_equalTo(w);
        make.centerX.mas_equalTo(0);
        make.top.mas_equalTo(APStatusBarHeight - ((h - 44.f) / 2));
    }];
}

///UI排布btn1 btn2
- (void)setRightItems:(NSArray<UIButton *> *)rightItems {
    _rightItems = rightItems;
    for (int i = 0; i < rightItems.count; i++) {
        UIButton *v = rightItems[i];
        [self.navigationBar addSubview:v];
        [v addAction:APSEL(rightButtonsAction:) onTarget:self];
        v.tag = BASE_TAG + i;
        let h = v.height?:44.f;
        [v mas_makeConstraints:^(MASConstraintMaker *make) {
            make.top.mas_equalTo(APStatusBarHeight - ((h - 44.f) / 2));
            make.height.mas_equalTo(h);
            make.width.mas_equalTo(v.width?:44);
            if (i == 0) {
                make.right.mas_equalTo(-16);
            }else {
                UIButton *pv = rightItems[i - 1];
                make.right.mas_equalTo(pv.mas_left);
            }
        }];
    }
}

- (void)returnButtonAction {
    [self popVC];
}

- (void)rightButtonsAction:(UIButton *)btn {
    [self rightItemsAction:btn.tag - BASE_TAG];
}

- (void)rightItemsAction:(NSInteger)index {}

- (void)networkResumed {}

- (void)dealloc {
    [self removeNotifications];
    APMethodLog
}

@end

//
//  APNavigationController.m
//  arpha plus
//
//  Created by michael on 2023/9/15.
//

#import "APNavigationController.h"

@interface APNavigationController ()

@end

@implementation APNavigationController

- (void)viewDidLoad {
    [super viewDidLoad];
}


- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated{
    if (self.childViewControllers.count) {
        viewController.hidesBottomBarWhenPushed = YES;
    }
    [super pushViewController:viewController animated:animated];
}

//- (UIViewController *)popViewControllerAnimated:(BOOL)animated {
//    return [super popViewControllerAnimated:animated];
//}
//
//- (NSArray<__kindof UIViewController *> *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated {
//    return [super popToViewController:viewController animated:animated];
//}
//
//- (NSArray<__kindof UIViewController *> *)popToRootViewControllerAnimated:(BOOL)animated {
//    return [super popToRootViewControllerAnimated:animated];
//}

@end

//
//  FWBaseEmptyView.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/11.
//

import UIKit
import SnapKit

class FWBaseEmptyView: UIView {

    enum Key: String {
        case image
        case title
        case button
        case eventBlock
        case imagePadding
        case buttonPadding
        case contentWidth
    }

    typealias EventBlock = () -> Void

    private var userInfo = [Key: Any]()

    // MARK: - Initialization

    convenience init(userInfo: [Key: Any]) {
        self.init(frame: .zero)
        self.userInfo = userInfo
        setupSubviews()
    }
    // MARK: - Setup

    private func setupSubviews() {
        let width = (userInfo[.contentWidth] as? CGFloat) ?? (UIScreen.main.bounds.width - 100)
        let imagePadding = (userInfo[.imagePadding] as? CGFloat) ?? 10
        let buttonPadding = (userInfo[.buttonPadding] as? CGFloat) ?? 10
        var subviews: [UIView] = []

        if let imageName = userInfo[.image] as? String {
            let imageView = UIImageView(image: UIImage(named: imageName))
            if userInfo[.button] == nil {
                let tapGesture = UITapGestureRecognizer(target: self, action: #selector(handleTap))
                imageView.addGestureRecognizer(tapGesture)
                imageView.isUserInteractionEnabled = true
            }
            subviews.append(imageView)
        }

        if let title = userInfo[.title] as? String {
            let label = UILabel()
            label.text = title
            label.font = .systemFont(ofSize: 15)
            label.textColor = .gray
            label.numberOfLines = 0
            label.textAlignment = .center
            subviews.append(label)
        }

        if let buttonInfo = userInfo[.button] {
            let button: UIButton
            if let customButton = buttonInfo as? UIButton {
                button = customButton
            } else if let buttonTitle = buttonInfo as? String {
                button = UIButton(type: .system)
                button.setTitle(buttonTitle, for: .normal)
            } else {
                fatalError("Invalid button type")
            }
            button.addTarget(self, action: #selector(handleTap), for: .touchUpInside)
            subviews.append(button)
        }

        for (index, view) in subviews.enumerated() {
            addSubview(view)
            view.snp.makeConstraints { make in
                make.centerX.equalToSuperview()
                if let imageView = view as? UIImageView {
                    make.size.equalTo(imageView.image?.size ?? .zero)
                } else if view is UILabel {
                    make.width.equalTo(width)
                } else if view is UIButton {
                    make.width.equalTo(width)
                    make.height.equalTo(50)
                }

                if index == 0 {
                    make.top.equalToSuperview()
                } else if index == subviews.count - 1 {
                    make.bottom.equalToSuperview()
                    make.top.equalTo(subviews[index - 1].snp.bottom).offset(index == 1 ? imagePadding : buttonPadding)
                } else {
                    make.top.equalTo(subviews[index - 1].snp.bottom).offset(imagePadding)
                }
            }
        }
    }

    // MARK: - Actions

    @objc private func handleTap() {
        if let eventBlock = userInfo[.eventBlock] as? EventBlock {
            eventBlock()
        }
    }

    // MARK: - Subscript

    subscript(key: Key) -> Any? {
        get {
            return userInfo[key]
        }
        set {
            userInfo[key] = newValue
            subviews.forEach { $0.removeFromSuperview() }
            setupSubviews()
            setNeedsUpdateConstraints()
        }
    }
}

//
//  FWConfirmPopView.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/16.
//

import UIKit

@objcMembers
class FWConfirmPopView: FWToastPopView {
    
    var buttonSpacing: CGFloat = 0
    var leftButtonWidth: CGFloat = 0
    
    private var usrInf: [FWPopProperty: Any]? {
        userInfo as? [FWPopProperty: Any]
    }
    
    private(set) lazy var cancelButton: UIButton = {
        let button = UIButton.apBorderButton(leftButton!, action: #selector(buttonAction(_:)), target: self, height: 45)
        button.setBImage(UIColor.popView)
        return button
    }()
    
    private(set) lazy var confirmButton: UIButton = {
        return UIButton.apGradientButton(rightButton!, action: #selector(buttonAction(_:)), target: self, height: 45)
    }()
    
    private var leftButton: Any!
    private var rightButton: Any!
    
    override func initVariable() {
        initConfirm()
        super.initVariable()
        
        let footer = UIView()
        footer.addSubviews([cancelButton, confirmButton])
        
        cancelButton.snp.makeConstraints { make in
            make.left.top.bottom.equalTo(0)
            if leftButtonWidth != 0 {
                make.width.equalTo(leftButtonWidth)
            } else {
                make.right.equalTo(confirmButton.snp.left).offset(-buttonSpacing)
            }
        }
        
        confirmButton.snp.makeConstraints { make in
            make.right.top.bottom.equalTo(0)
            if leftButtonWidth != 0 {
                make.left.equalTo(cancelButton.snp.right).offset(buttonSpacing)
            } else {
                make.width.equalTo(cancelButton.snp.width)
            }
        }
        
        self.footer = footer
        fitHeight = true
        footerPadding = 20
        topPadding = 20
        
        if type == 0 && usrInf?[.touchOutside] == nil {
            touchOutside = false
        }
    }
    
    private func initConfirm() {
        if let usrInf {
            leftButton = usrInf[.leftButton] ?? "Cancel".local
            rightButton = usrInf[.rightButton] ?? "Confirm".local
            buttonSpacing = (usrInf[.buttonSpacing] as? CGFloat) ?? 20
            leftButtonWidth = usrInf[.leftButtonWidth] as? CGFloat ?? 0
        }
    }
    
    @objc func buttonAction(_ btn: UIButton) {
        dismiss(btn == cancelButton ? nil : 1)
    }
}

//
//  FWPopView.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/16.
//

import UIKit

public typealias FWPopProperty = String

public extension FWPopProperty {
    static let contentWidth = "FWPopContentWidth"
    static let contentHeight = "FWPopContentHeight"
    static let contentBottom = "FWPopContentBottom"
    static let touchOutside = "FWPopTouchOutside"
    static let cornerRadius = "FWPopCornerRadius"
    static let bottomCorner = "FWPopBottomCorner"
    static let type = "FWPopType"
    static let eventBlock = "FWPopEventBlock"
    static let topMargin = "FWPopTopMargin"
    static let bottomMargin = "FWPopBottomMargin"
    static let topPadding = "FWPopTopPadding"
    static let bottomPadding = "FWPopBottomPadding"
    static let headerWidth = "FWPopHeaderWidth"
    static let headerHeight = "FWPopHeaderHeight"
    static let headerPadding = "FWPopHeaderPadding"
    static let headerHorizon = "FWPopHeaderHorizon"
    static let headerAlign = "FWPopHeaderAlign"
    static let bodyWidth = "FWPopBodyWidth"
    static let bodyHeight = "FWPopBodyHeight"
    static let bodyPadding = "FWPopBodyPadding"
    static let bodyHorizon = "FWPopBodyHorizon"
    static let bodyAlign = "FWPopBodyAlign"
    static let footerWidth = "FWPopFooterWidth"
    static let footerHeight = "FWPopFooterHeight"
    static let footerPadding = "FWPopFooterPadding"
    static let footerHorizon = "FWPopFooterHorizon"
    static let fitHeight = "FWPopFitHeight"
    static let ignoreDismiss = "FWPopIgnoreDismiss"
    static let buttonSpacing = "FWPopButtonSpacing"
    static let leftButton = "FWPopLeftButton"
    static let leftButtonWidth = "FWPopLeftButtonWidth"
    static let rightButton = "FWPopRightButton"
    static let header = "FWPopHeader"
    static let body = "FWPopBody"
    static let footer = "FWPopFooter"
    static let timeout = "FWPopTimeout"
    static let timeoutBlock = "FWPopTimeoutBlock"
}

@objcMembers
public class FWPopView: UIView {
    
    public var ignoreDismiss: Bool = false
    public var contentWidth: CGFloat = 0
    public var contentHeight: CGFloat = 0
    public var contentBottom: CGFloat = 0
    public var touchOutside: Bool = false
    public var bottomCorner: Bool = false
    public var cornerRadius: CGFloat = 0
    public var type: Int = 0 {
        didSet {
            cornerRadius = 32
            touchOutside = true
            contentWidth = APScreenW - 40
            contentBottom = 20
        }
    }
    public var eventBlock: ((Any?) -> Void)?
    
    public private(set) var contentView: UIView!
    public var userInfo: Any?
    
    private var backgroundView: UIView!
    
    private static let BASE_TAG = 290747
    
    public required init(userInfo: Any?, eventBlock: ((Any?) -> Void)? = nil) {
        super.init(frame: UIScreen.main.bounds)
        self.userInfo = userInfo
        self.eventBlock = eventBlock
        setupView()
    }
    
    public required init(properties: [FWPopProperty: Any]?, eventBlock: ((Any?) -> Void)? = nil) {
        super.init(frame: UIScreen.main.bounds)
        self.userInfo = properties
        self.eventBlock = eventBlock
        setupView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    private func setupView() {
        backgroundView = UIView(color: .black.alpha(0))
        
        initVariable()
        
        backgroundView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(backgroundTapped)))
        
        contentView = UIView(color: .popView)
        
        if cornerRadius > 0 {
            contentView.clipsToBounds = true
            contentView.addCorner(cornerRadius)
        }
        
        if type == 0 {
            contentView.transform = CGAffineTransform(scaleX: 0.7, y: 0.7)
            contentView.alpha = 0
        }
        addSubviews([backgroundView,contentView])
        backgroundView.snp.makeConstraints { make in
            make.edges.equalToSuperview()
        }
        contentView.snp.makeConstraints { make in
            make.centerX.equalToSuperview()
            if type == 1 {
                make.bottom.equalTo(contentHeight + contentBottom)
            } else {
                make.centerY.equalToSuperview()
            }
            make.width.equalTo(contentWidth)
            make.height.equalTo(contentHeight)
        }
    
        initContentUI()
    }
    
    @objc private func backgroundTapped() {
        if touchOutside {
            dismiss()
        }
    }
    
    public func initVariable() {
        contentWidth = UIScreen.main.bounds.width - 70
        cornerRadius = 24
    }
    
    public func initContentUI() {
        // Override in subclass if needed
    }
    
    public func show() {
        if type == 1 {
            contentView.snp.updateConstraints { make in
                make.bottom.equalTo(-contentBottom)
            }
            UIView.animate(withDuration: 0.3) {
                self.layoutIfNeeded()
                self.backgroundView.backgroundColor = .black.alpha(0.5)
            }
        } else {
            UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 0.8, initialSpringVelocity: 15, options: .curveLinear) {
                self.backgroundView.backgroundColor = .black.alpha(0.5)
                self.contentView.transform = .identity
                self.contentView.alpha = 1
            }
        }
    }
    
    public func dismiss(_ userInfo: Any? = nil, triggerEvent: Bool = true, animated: Bool = true) {
        if triggerEvent {
            eventBlock?(userInfo)
        }
        eventBlock = nil
        
        if animated {
            if type == 1 {
                contentView.snp.updateConstraints { make in
                    make.bottom.equalTo(contentHeight + contentBottom)
                }
            }
            UIView.animate(withDuration: 0.3, animations: {
                self.backgroundView.backgroundColor = .black.alpha(0)
                if self.type == 1 {
                    self.layoutIfNeeded()
                } else {
                    self.contentView.alpha = 0
                }
            }) { _ in
                self.removeFromSuperview()
            }
        } else {
            removeFromSuperview()
        }
    }
    
    @discardableResult
    public class func pop(userInfo: Any, on view: UIView? = nil, eventBlock: ((Any?) -> Void)? = nil) -> Self {
        dismiss(on: view, animated: false)
        let popView = Self.init(userInfo: userInfo, eventBlock: eventBlock)
        popView.pop(on: view)
        return popView
    }
    
    @discardableResult
    public class func pop(properties: [FWPopProperty: Any], on view: UIView? = nil, eventBlock: ((Any?) -> Void)? = nil) -> Self {
        dismiss(on: view, animated: false)
        let popView = Self.init(properties: properties, eventBlock: eventBlock)
        popView.pop(on: view)
        return popView
    }
    
    @discardableResult
    public class func pop(on view: UIView? = nil, eventBlock: ((Any?) -> Void)? = nil) -> Self {
        dismiss(on: view, animated: false)
        let popView = Self.init(userInfo: nil, eventBlock: eventBlock)
        popView.pop(on: view)
        return popView
    }
    
    @objc public class func forcePop(_ properties: [FWPopProperty: Any], on view: UIView? = nil, eventBlock: ((Any?) -> Void)? = nil) {
        var info = properties
        info[.header] = info[.header] == nil ? "" : info[.header]
        info[.footer] = info[.footer] == nil ? "" : info[.footer]
        self.pop(properties: info, on: view, eventBlock: eventBlock)
    }

    public func pop(on view: UIView? = nil) {
        let superView = view ?? UIApplication.shared.windows.first { $0.isKeyWindow }
        tag = FWPopView.BASE_TAG
        superView?.addSubview(self)
        dispatch_after_on_main_queue(0.1) {
            self.show()
        }
    }
    
    public class func dismiss(on view: UIView? = nil, animated: Bool) {
        popViewDismiss(UIApplication.shared.windows.first { $0.isKeyWindow }?.viewWithTag(BASE_TAG), animated: animated)
        if let view = view, view != UIApplication.shared.windows.first(where: { $0.isKeyWindow }) {
            popViewDismiss(view.viewWithTag(BASE_TAG), animated: animated)
        }
    }
    
    private class func popViewDismiss(_ popView: UIView?, animated: Bool) {
        if let popView = popView as? FWPopView, !popView.ignoreDismiss {
            popView.dismiss(animated: animated)
        }
    }
    
    public class func isVisible(on view: UIView? = nil) -> Bool {
        return (view ?? UIApplication.shared.windows.first { $0.isKeyWindow })?.viewWithTag(BASE_TAG) != nil
    }
}

//
//  FWToastPopView.swift
//  FuweiCloud
//
//  Created by michael on 2024/10/16.
//

import UIKit

enum FWToastPopViewType: UInt {
    case none
    case loading
    case failed
    case succeeded
}

@objcMembers
class FWToastPopView: FWPopView {
    
    // MARK: - Properties
    
    var topMargin: CGFloat = 20
    var bottomMargin: CGFloat = 20
    var topPadding: CGFloat = 20
    var bottomPadding: CGFloat = 20
    var headerPadding: CGFloat = 30
    var headerWidth: CGFloat = 64
    var headerHeight: CGFloat = 0
    var headerHorizon: Bool = false
    var headerAlign: NSTextAlignment = .center
    var bodyPadding: CGFloat = 30
    var bodyWidth: CGFloat = 60
    var bodyHeight: CGFloat = 100
    var bodyHorizon: Bool = false
    var bodyAlign: NSTextAlignment = .center
    var footerPadding: CGFloat = 30
    var footerWidth: CGFloat = 160
    var footerHeight: CGFloat = 45
    var footerHorizon: Bool = false
    var fitHeight: Bool = false
    
    var header: Any?
    var body: Any?
    var footer: Any?
    var timeout: TimeInterval = 0
    var timeoutBlock: (() -> Void)?
    
    private(set) var headerView: UIView?
    private(set) var bodyView: UIView?
    private(set) var footerView: UIView?
    
    private var timer: YYTimer?
    
    // MARK: - Initialization
    
    override func initVariable() {
        super.initVariable()
        initToast()
        if let userInfo = userInfo as? [String: Any] {
            for (key, value) in userInfo where key.hasPrefix("FWPop") {
                let k = String(key.dropFirst(5))
                if responds(to: Selector("set\(k):")) {
                    setValue(value, forKey: k.nsString.uncapitalized())
                }
            }
        }
        if timeout > 0 {
            timer = YYTimer(timeInterval: timeout, target: self, selector: #selector(timeoutAction), repeats: false)
        }
    }
    
    private func initToast() {
        contentWidth = min(APScreenW, APScreenH) - 90
        contentHeight = 220
    }
    
    // MARK: - UI Setup
    
    override func initContentUI() {
        headerView = viewWithData(header, type: 0)
        bodyView = viewWithData(body, type: 1)
        footerView = viewWithData(footer, type: 2)
        
        let count = [headerView, bodyView, footerView].compactMap { $0 }.count
        var insetSpace: CGFloat = 0
        
        switch count {
        case 3:
            insetSpace = topPadding + bottomPadding
        case 2:
            if headerView != nil {
                if footerView != nil && headerHeight < 30 {
                    topPadding = min(40, topPadding + 20)
                }
                insetSpace = topPadding
            } else if bodyView != nil {
                if footerView != nil && bodyHeight < 30 {
                    bottomPadding = min(40, bottomPadding + 20)
                }
                insetSpace = bottomPadding
            }
        default:
            break
        }
        
        var fitHeight = headerHeight + bodyHeight + footerHeight + insetSpace + topMargin + bottomMargin
        if !self.fitHeight {
            let diff = max(0, (contentHeight - fitHeight) / 2)
            topMargin += diff
            bottomMargin += diff
            fitHeight = max(contentHeight, fitHeight)
        }
        
        contentHeight = fitHeight
        
        contentView.snp.updateConstraints { make in
            make.height.equalTo(fitHeight)
        }
        
        setupHeaderView()
        setupBodyView()
        setupFooterView()
    }
    
    private func setupHeaderView() {
        guard let headerView = headerView else { return }
        contentView.addSubview(headerView)
        headerView.snp.makeConstraints { make in
            if headerHorizon {
                make.width.equalTo(headerWidth)
                make.centerX.equalToSuperview()
            } else {
                make.left.equalTo(headerPadding)
                make.right.equalTo(-headerPadding)
            }
            make.top.equalTo(topMargin)
            make.height.equalTo(headerHeight)
        }
    }
    
    private func setupBodyView() {
        guard let bodyView = bodyView else { return }
        contentView.addSubview(bodyView)
        bodyView.snp.makeConstraints { make in
            if bodyHorizon {
                make.width.equalTo(bodyWidth)
                make.centerX.equalToSuperview()
            } else {
                make.left.equalTo(bodyPadding)
                make.right.equalTo(-bodyPadding)
            }
            if headerView != nil {
                make.top.equalTo(headerView!.snp.bottom).offset(topPadding)
            } else {
                make.top.equalTo(topMargin)
            }
            make.height.equalTo(bodyHeight)
        }
    }
    
    private func setupFooterView() {
        guard let footerView = footerView else { return }
        contentView.addSubview(footerView)
        footerView.snp.makeConstraints { make in
            if footerHorizon {
                make.width.equalTo(footerWidth)
                make.centerX.equalToSuperview()
            } else {
                make.left.equalTo(footerPadding)
                make.right.equalTo(-footerPadding)
            }
            make.bottom.equalTo(-bottomMargin)
            make.height.equalTo(footerHeight)
        }
    }
    
    // MARK: - Helper Methods
    
    private func viewWithData(_ data: Any?, type: Int) -> UIView? {
        guard let data = data else {
            switch type {
            case 0: headerHeight = 0
            case 1: bodyHeight = 0
            case 2: footerHeight = 0
            default: break
            }
            return nil
        }
        
        if let view = data as? UIView {
            return view
        } else if data is Int || data is FWToastPopViewType {
            let num = if let n = data as? Int {
                n
            } else if let type = data as? FWToastPopViewType {
                Int(type.rawValue)
            } else {
                0
            }
            if type == 0 {
                headerHorizon = true
                headerHeight = 64
            }
            switch  num {
            case 1: return UIImageView.apLoading()
            case 2: return UIImageView(named: "toast_error")
            case 3: return UIImageView(named: "toast_success")
            default: return nil
            }
        } else if let image = data as? UIImage {
            if type == 2 {
                return UIButton(image: image)
            }
            if type == 0 {
                headerHeight = 64
            }
            return UIImageView(image: image)
        } else if data is String || data is AString {
            let text = if let str = data as? String {
                str.isEmpty ? (type == 2 ?"Got it".local:"Tips".local) : str
            } else {
                data
            }
            switch type {
            case 0:
                let label = YYLabel(string: text, font: 17.6, color: .label)
                label.numberOfLines = 0
                label.textAlignment = headerAlign
                headerHeight = max(label.textHeight(forWidth: contentWidth - headerPadding * 2), headerHeight)
                return label
            case 1:
                let textView = YYTextView(string: text, font: 15.6, color: .label)
                textView.textAlignment = bodyAlign
                textView.isEditable = false
                bodyHeight = min(bodyHeight, textView.textHeight(forWidth: contentWidth - bodyPadding * 2))
                return textView
            case 2:
                footerHorizon = true
                let button = UIButton.apGradientButton(text, action: APSEL("buttonAction"), target: self, height: 45)
                return button
            default:
                return nil
            }
        } else {
            return nil
        }
    }
    
    // MARK: - Actions
    
    @objc private func buttonAction() {
        dismiss()
    }
    
    @objc private func timeoutAction() {
        timeoutBlock?()
        timeoutBlock = nil
        dismiss()
    }
    
    // MARK: - Deinitialization
    
    deinit {
        timer?.invalidate()
        timer = nil
        print("APToastPopView deinitialized")
    }
}

//
//  FWAppDelegate.swift
//  FuweiCloud
//
//  Created by michael on 2024/6/21.
//

import UIKit
import UserNotifications
import Bugly
import FirebaseCore

@UIApplicationMain
class FWAppDelegate: UIResponder, UIApplicationDelegate {
    var state: UIApplication.State {
        UIApplication.shared.applicationState
    }
    var window: UIWindow?
    @objc let network: YYReachability = {
        YYReachability()
    }()
    var allowRotate: Bool = false
    var isLaunched: Bool = false
    
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        setupUI()
        configDebug()
        setupAPNs()
        setupKeyboard()
        return true
    }
    func applicationDidBecomeActive(_ application: UIApplication) {
        isLaunched = true
    }
    
    
    func applicationWillEnterForeground(_ application: UIApplication) {
        ///< 检查通知权限
        UNUserNotificationCenter.current().getNotificationSettings { settings in
            dispatch_async_on_main_queue {
                if settings.authorizationStatus == .denied {
                    FWConfirmPopView.pop(properties: [
                        .header: "please open mobile notify auth first".local,
                        .rightButton: "Setting".local,
                    ]) { userInfo in
                        if nil == userInfo { return }
                        APAppSettingURLHelper.gotoURL(.appSetting)
                    }
                }
            }
        }
    }
    
    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        return WXApi.handleOpen(url, delegate: APWechatLoginViewModel.shareInstance())
    }
    
    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        return allowRotate ? .allButUpsideDown : .portrait
    }
    
    func application(_ application: UIApplication, shouldAllowExtensionPointIdentifier extensionPointIdentifier: UIApplication.ExtensionPointIdentifier) -> Bool {
        return extensionPointIdentifier != "com.apple.keyboard-service"
    }
    
    private func setupUI() {
        window = UIWindow(frame: UIScreen.main.bounds)
        window?.backgroundColor = .white
        window?.makeKeyAndVisible()
        let logined = APGlobal.userInfo != nil || APGlobal.homeKit
        let rootVC = logined ? FWMainTabBarController() : FWLoginViewController()
        window?.rootViewController = APNavigationController(rootViewController: rootVC)
        let launchView = FWLaunchScreenView()
        window?.addSubview(launchView)
        dispatch_after_on_main_queue(1) {
            launchView.removeFromSuperview()
        }
    }
    
    private func setupKeyboard() {
        let manager = IQKeyboardManager.shared()
        manager.isEnabled = true
        manager.shouldResignOnTouchOutside = true
        manager.shouldToolbarUsesTextFieldTintColor = true
        manager.isEnableAutoToolbar = true
    }
    
    private func configDebug() {
        network.notifyBlock = { [unowned self] reachability in
            if reachability.isReachable {
                if !APGlobal.deviceTokenBinded {
                    bindAPNsToken()
                }
                FWMQTTManager.shared.startConnect()
            }
            NotiCenter.post(name: .networkChanged, object: reachability.isReachable)
        }
        
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy/MM/dd HH:mm:ss:SSS"
        dateFormatter.locale = NSLocale.system
        let logFormatter = DDLogFileFormatterDefault(dateFormatter: dateFormatter)
        ///< Wechat
        WXApi.registerApp(AppWXAppID, universalLink: AppWXUniversalLink)
        ///< APGlobal
        APGlobal.talkType = 1
        
        #if ARPHA_PLUS
        FirebaseApp.configure()
        #endif
        ///< Logger
        let osLogger = DDOSLogger(subsystem: logSubsystemName, category: logCategoryName)
//        osLogger.logFormatter = logFormatter
        DDLog.add(osLogger)
        
        #if !DEBUG  /// 正式环境上报
        ///< Bugly配置
        Bugly.start(withAppId: AppBuglyID)
        ///< DDLog
        let logPath = "\(APFileManager.pathForDocumentsDirectory())/\(ArphaPlusPath)/Logs"
        let mgr = DDLogFileManagerDefault(logsDirectory: logPath)
        mgr.maximumNumberOfLogFiles = 1
        mgr.logFilesDiskQuota = 20 * 1024 * 1024
        let fileLogger = DDFileLogger(logFileManager: mgr)
        fileLogger.rollingFrequency = 0
        fileLogger.logFormatter = logFormatter
        fileLogger.maximumFileSize = 20 * 1024 * 1024
        DDLog.add(fileLogger)
        #endif
    }
    
    
    /// 绑定APNs推送的token
    private func bindAPNsToken() {
        if APGlobal.userInfo == nil {
            DDLogError("can not bind device token due to null userInfo");
            return
        }
        if APGlobal.deviceToken.isEmpty {
            DDLogError("device token is empty!")
            return
        }
        let token = APGlobal.deviceToken
        let debug = Int(isDebug)
        _ = APRequest(api: .loginAPNsRefresh, parameters: [
            "device_token": token,
            "reg_token": token,
            "AG": "APNS",
            "ios_dev": debug
        ]) { results, message, code in
            if nil == message {
                DDLogInfo("bind device_token:\(token) is_dev:\(debug) success")
                APGlobal.deviceTokenBinded = true
            }else {
                DDLogInfo("bind device_token:\(token) is_dev:\(debug) failed")
            }
        }
    }
}

//MARK: - APNs
extension FWAppDelegate: UNUserNotificationCenterDelegate {
    
   private func setupAPNs() {
        let notiCenter = UNUserNotificationCenter.current()
        notiCenter.delegate = self
        notiCenter.requestAuthorization(options: [.sound, .alert, .badge]) { granted, error in
            if granted { return }
            dispatch_async_on_main_queue {
                FWConfirmPopView.pop(properties: [
                    .header: "please open mobile notify auth first".local,
                    .rightButton: "Setting".local,
                ]) { userInfo in
                    if nil == userInfo { return }
                    APAppSettingURLHelper.gotoURL(.appSetting)
                }
            }
        }
        UIApplication.shared.registerForRemoteNotifications()
    }
    
    private func handleAPNsUserInfo(_ userInfo: APNsUserInfo) {
        dispatch_after_on_main_queue(isLaunched ?0.3:1.5) {
            APDeviceModel.device(withKeyId: userInfo.payload.deviceKeyId(), isOwner: true) { [unowned self] device in
                if let device {
                    handlePageRouter(userInfo: userInfo, device: device)
                }
            }
        }
    }
    
    private func handlePageRouter(userInfo: APNsUserInfo, device: APDeviceModel) {
        var cvc: UIViewController? = UIViewController.currentTop()
        if let nvc = cvc as? UINavigationController {
            cvc = nvc.topViewController
        }
        guard let cvc else { return }
        FWPopView.dismiss(animated: false)
        
        if userInfo.payload.outdated { ///< 旧版推送
            switch userInfo.event?.type {
            case .doorbell:
                device.gotoRemoteCallPage(cvc)
            default:
                APEventListController.jump(withDevice: nil)
            }
        }else {
            switch userInfo.payload.action {
            case .livePage:
                device.gotoRemoteCallPage(cvc)
            default:
                APEventListController.jump(withDevice: nil)
            }
        }
    }
    /// 从系统SDK获取设备的APNs推送token
    /// - Parameters:
    ///   - application: 当前应用
    ///   - deviceToken: 设备token数据
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let deviceTokenString = deviceToken.hexString.lowercased()
        if deviceTokenString.isEmpty {
            DDLogError("system generate deviceToken error")
            return
        }
        DDLogInfo("system generate deviceToken:\(deviceTokenString)")
        APGlobal.deviceToken = deviceTokenString
        if APGlobal.userInfo != nil {
            bindAPNsToken()
        }
    }
    
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) {
        DDLogError("didFailToRegisterForRemoteNotificationsWithError:\(error)")
    }
    
    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
        DDLogInfo("apsTag: background remote notification:\(userInfo)");
        handleAPNsUserInfo(APNsUserInfo(dict: userInfo))
    }
    
    /// 前台推送回调方法
    /// - Parameters:
    ///   - center: 推送中心
    ///   - notification: 推送信息
    ///   - completionHandler: 回调处理
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let userInfo = notification.request.content.userInfo
        let apns = APNsUserInfo(dict: userInfo)
        DDLogInfo("apsTag: foreground remote notification:\(userInfo)");
        let isALSerialRemoteCall = apns.payload.outdated && apns.isRemoteCall
        let option: UNNotificationPresentationOptions = isALSerialRemoteCall ? [.badge,.sound] : [.badge,.sound,.alert]
        if isALSerialRemoteCall {
            handleAPNsUserInfo(apns)
        }
        completionHandler(option)
    }
}

//MARK: - google universial link 和 alexa universial link
extension FWAppDelegate {
    
    ///< 处理 Alexa 跳转
    func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([any UIUserActivityRestoring]?) -> Void) -> Bool {
        DDLogInfo("webpage url:\(userActivity.webpageURL as Any)")
        if APGlobal.userInfo == nil || userActivity.activityType != NSUserActivityTypeBrowsingWeb { return false }
        if let webpageURL = userActivity.webpageURL,
           let components = URLComponents(url: webpageURL, resolvingAgainstBaseURL: true),
           let queryItems = components.queryItems, queryItems.count > 0
        {
            let isAlexa = webpageURL.absoluteString.contains("alexa")
            if isAlexa {
                let code = queryItems.first(where: { $0.name == "code" })?.value
                let error = queryItems.first(where: { $0.name == "error" })?.value
                if code != nil || error != nil {
                    if error != "access_denied" {
                        UIViewController.current().pushVC(APAlexaLinkController(code: code, error: error))
                    }
                    return true
                }
                return false
            }else {
                if let clientId = queryItems.filter({$0.name == "client_id"}).first?.value,
                    let state = queryItems.filter({$0.name == "state"}).first?.value,
                    let redirectUri = queryItems.filter({$0.name == "redirect_uri"}).first?.value,
                    googleAuthCode.count > 0 && googleClientId.count > 0 {
                    returnGoogleAuthCode(code: googleAuthCode, state: state, redirectUri: redirectUri)
                    return (clientId == googleClientId)
                } else {
                    if let redirectUri = queryItems.filter({$0.name == "redirect_uri"}).first?.value {
                        returnGoogleError(redirectUri: redirectUri)
                    }
                    return false
                }
            }
            
        }
        return false
    }
    
    private func returnGoogleAuthCode(code: String, state: String, redirectUri: String) {
        guard let redirectURL = URL(string: redirectUri) else { return }
        var components = URLComponents(url: redirectURL, resolvingAgainstBaseURL: false)
        // Return the authorization code and original state
        let paramAuthCode = URLQueryItem(name: "code", value: code)
        let paramState = URLQueryItem(name: "state", value: state)
        components?.queryItems = [paramAuthCode, paramState]
        if let resultURL = components?.url {
            print("google auth code url:\(resultURL)")
            UIViewController.current().pop(toRootVC: false)
            dispatch_after_on_main_queue(0.1, {
                UIViewController.current().pushVC(FWGAAuthController(url: resultURL))
            })
        }
    }
    
    private func returnGoogleError(redirectUri: String) {
        guard let redirectURL = URL(string: redirectUri) else { return }
        var components = URLComponents(url: redirectURL, resolvingAgainstBaseURL: false)

        // Return the authorization code and original state
        let paramError = URLQueryItem(name: "error", value: "invalid_request")
        let paramDescription = URLQueryItem(name: "error_description", value: "Invalid Request")
        components?.queryItems = [paramError, paramDescription]
        if let resultURL = components?.url {
            print("google redirect url:\(resultURL)")
            UIApplication.shared.open(
                resultURL,
                options: [.universalLinksOnly : true],
                completionHandler: nil)
        }
    }
    
}


